Go言語を学び始めて1ヶ月の私が、Oracle DBのCRUD処理を実装する必要があったので、記事を書いてみようと思います。
1. はじめに
1.1. 読んでみてほしい対象者
- Go言語でOracle DB接続を実装したい人
- OR Mapperを利用できないため、簡易的にDB処理を実装したい人
1.2 利用するOracle Driver
今回利用するOracle Driverはsijms/go-ora
です。
sijms/go-ora
はv1とv2がありますが、v2を利用します。
(Oracle 10.2以降はv2の利用を推奨と公式でも書かれています。)
私が利用を検討した結果のsijms/go-ora
のメリット/デメリットは下記です。
- メリット
- 導入が簡単
- Go言語のみで作成されたOracle Driverのため、
Oracle Instant Client
などのインストールは不要で、go get
コマンドさえ打てばすぐ使えます。
ちなみに、sijms/go-ora
以外のOracle Driverで環境構築が大変な話は下記の記事が参考になると思います。
- Go言語のみで作成されたOracle Driverのため、
- コンテナのイメージサイズ軽量化
- 他のOracle Driverの場合、
Oracle Instant Client
が必要なため、コンテナ化するとイメージサイズが大きくなってしまいます。 - 例 : 「sijms/go-ora」と「mattn/go-oci8」のコンテナイメージサイズの比較
-
sijms/go-ora
: 13.9MB -
mattn/go-oci8
: 1.5GB
-
- 他のOracle Driverの場合、
- 導入が簡単
- デメリット
1.3. 環境情報
環境 | バージョン |
---|---|
OS | Windows10 Home 21H2 |
Golang | 19.1 |
Oracle | 19c(19.3.0) |
1.4. 動作確認するテーブル情報
下記のような商品を模したテーブルを作成して、OracleDBの動作確認をしてみます。
CREATE TABLE items (
id VARCHAR2(3) NOT NULL,
name VARCHAR2(10) NOT NULL,
price INT NOT NULL,
description VARCHAR2(20),
constraint items_pk primary key (id)
)
2. Entity定義編
構造体(struct)を用いてEntity定義を行います。
NOT NULL制約のある項目は、Go言語標準の型(int, string, etc..)を利用すれば良いのですが、
NOT NULL制約のない項目は、database/sqlで用意された型(NullInt32, NullString, etc..)を利用しましょう。
(database/sqlの型を利用しないと、nullをSELECTした時などにエラーが起きます。)
type Item struct {
Id string
Name string
Price int
Description sql.NullString
}
3. DB Open + DML編
3.1. DB Open
sql.Open()
関数を利用して、DBをオープンします。
DataSourceNameは、go-oraで用意されたBuildUrl()
関数があるので、これを利用すると実装が楽です。
package main
import (
"database/sql"
"log"
go_ora "github.com/sijms/go-ora/v2"
)
func main() {
db, err := open()
if err != nil {
return
}
defer db.Close()
}
func open() (*sql.DB, error) {
dataSourceName := go_ora.BuildUrl("localhost", 1521, "SAMPLEPDB", "username", "password", nil)
db, err := sql.Open("oracle", dataSourceName)
if err != nil {
log.Fatalln("error:", err)
return nil, err
}
return db, err
}
3.2. Insert
sql.DBのExec()
関数を利用して、SQLを実行します。
type Item struct { /* 省略 */ }
func main() {
db, err := open()
if err != nil {
log.Fatalln("DB Open Error:", err)
return
}
defer db.Close()
item := Item{Id: "001", Name: "pen", Price: 100, Description: sql.NullString{String: "Color is black.", Valid: true}}
if err := insert(db, item); err != nil {
log.Fatalln("Insert Error:", err)
return
}
}
func open() (*sql.DB, error) { /* 省略 */}
func insert(db *sql.DB, item Item) error {
_, err := db.Exec(
"INSERT INTO items (id, name, price, description) VALUES (:1, :2, :3, :4)",
item.Id, item.Name, item.Price, item.Description.String,
)
return err
}
実行後のDB結果
ID NAME PRICE DESCRIPTION
--- ---------- ---------- --------------------
001 pen 100 Color is black.
3.3. Select
sql.DBのQuery()
関数を利用して、SQLを実行します。
Entityへのマッピング処理は、sql.RowsのNext()
, Scan()
関数を使います。
type Item struct { /* 省略 */ }
func main() {
db, err := open()
if err != nil {
log.Fatalln("DB Open Error:", err)
return
}
defer db.Close()
items, err := find(db)
if err != nil {
log.Fatalln("Select Error:", err)
return
}
log.Println(items)
}
func open() (*sql.DB, error) { /* 省略 */}
func find(db *sql.DB) ([]Item, error) {
rows, err := db.Query("SELECT id, name, price, description FROM items")
if err != nil {
return nil, err
}
// Item Entityへのマッピング処理
var items []Item
for rows.Next() {
item := &Item{}
if err := rows.Scan(&item.Id, &item.Name, &item.Price, &item.Description); err != nil {
return nil, err
}
items = append(items, *item)
}
return items, nil
}
実行後のコンソールログ
2022/11/30 18:07:21 [{001 pen 100 {Color is black. true}}]
3.4. Update
sql.DBのExec()
関数を利用して、SQLを実行します。
更新された行数を取得したい場合は、RowsAffected()
関数を利用します。
type Item struct { /* 省略 */ }
func main() {
db, err := open()
if err != nil {
log.Fatalln("DB Open Error:", err)
return
}
defer db.Close()
if err := update(db, "001", 200); err != nil {
log.Fatalln("Upadate Error:", err)
return
}
}
func open() (*sql.DB, error) { /* 省略 */}
func update(db *sql.DB, id string, price int) error {
result, err := db.Exec(
"UPDATE items SET price = :1 WHERE id = :2",
price, id,
)
if err != nil {
return err
}
affectedRowNum, err := result.RowsAffected()
if err != nil {
return err
} else if affectedRowNum != 1 { // id指定によるupdateのため、更新レコード数が1以外はエラー
return errors.New("number of updated rows is incorrect")
}
return err
}
実行後のDB結果
ID NAME PRICE DESCRIPTION
--- ---------- ---------- --------------------
001 pen 200 Color is black.
3.5. Delete
sql.DBのExec()
関数を利用して、SQLを実行します。
type Item struct { /* 省略 */ }
func main() {
db, err := open()
if err != nil {
log.Fatalln("DB Open Error:", err)
return
}
defer db.Close()
if err := delete(db, "001"); err != nil {
log.Fatalln("Delete Error:", err)
return
}
}
func open() (*sql.DB, error) { /* 省略 */}
func delete(db *sql.DB, id string) error {
_, err := db.Exec("DELETE FROM items WHERE id = :1", id)
return err
}
実行後のDB結果
ID NAME PRICE DESCRIPTION
--- ---------- ---------- --------------------
4. DDL編
4.1. Create Table
sql.DBのExec()
関数を利用して、SQLを実行します。
func main() {
db, err := open()
if err != nil {
log.Fatalln("DB Open Error:", err)
return
}
defer db.Close()
if err := createTable(db); err != nil {
log.Fatalln("Create Table Error:", err)
return
}
}
func open() (*sql.DB, error) { /* 省略 */}
func createTable(db *sql.DB) error {
_, err := db.Exec(
`CREATE TABLE items (
id VARCHAR2(3) NOT NULL,
name VARCHAR2(10) NOT NULL,
price INT NOT NULL,
description VARCHAR2(20),
constraint items_pk primary key (id)
)`,
)
return err
}
実行後のDB結果
TABLE_NAME
--------------------------------------------------------------------------------
ITEMS
4.2. Drop Table
sql.DBのExec()
関数を利用して、SQLを実行します。
func main() {
db, err := open()
if err != nil {
log.Fatalln("DB Open Error:", err)
return
}
defer db.Close()
if err := dropTable(db); err != nil {
log.Fatalln("Drop Table Error:", err)
return
}
}
func open() (*sql.DB, error) { /* 省略 */}
func dropTable(db *sql.DB) error {
_, err := db.Exec("DROP TABLE items")
return err
}
実行後のDB結果
TABLE_NAME
--------------------------------------------------------------------------------
5. DCL編
5.1. Rollback
トランザクションの開始はsql.DBBegin()
関数を使い、返り値のsql.Txでトランザクション制御を行います。
3, 4章では、sql.DBのQuery()
やExec()
を利用していましたが、
トランザクション制御する場合は、sql.TxのQuery()
やExec()
を利用する必要があります。
ロールバックはsql.TxのRollback()
関数を使います。
type Item struct { /* 省略 */ }
func main() {
db, err := open()
if err != nil {
log.Fatalln("DB Open Error:", err)
return
}
defer db.Close()
if err := rollback(db); err != nil {
log.Fatalln("Rollback Error:", err)
return
}
}
func open() (*sql.DB, error) { /* 省略 */}
func rollback(db *sql.DB) error {
tx, err := db.Begin()
if err != nil {
return err
}
_, err = tx.Exec("INSERT INTO items (id, name, price) VALUES ('001', 'pen', 100)")
if err != nil {
return err
}
return tx.Rollback()
}
実行後のDB結果
ID NAME PRICE DESCRIPTION
--- ---------- ---------- --------------------
5.2. Commit
トランザクションの開始はsql.DBBegin()
関数を使い、コミットはsql.TxのCommit()
関数を使います。
type Item struct { /* 省略 */ }
func main() {
db, err := open()
if err != nil {
log.Fatalln("DB Open Error:", err)
return
}
defer db.Close()
if err := commit(db); err != nil {
log.Fatalln("Commit Error:", err)
return
}
}
func open() (*sql.DB, error) { /* 省略 */}
func commit(db *sql.DB) error {
tx, err := db.Begin()
if err != nil {
return err
}
_, err = tx.Exec("INSERT INTO items (id, name, price) VALUES ('001', 'pen', 100)")
if err != nil {
return err
}
return tx.Commit()
}
実行後のDB結果
ID NAME PRICE DESCRIPTION
--- ---------- ---------- --------------------
001 pen 100
6. Timeout編
6.1 Query Timeout
contextのWithTimeout()
で指定した時間を経過後に、クエリの実行を中断させることができます。
contextを設定して、クエリを実行するためには、QueryContext()
or ExecContext()
関数を使います。
サンプルでは、タイムアウト時間を極限まで小さくすることで、タイムアウトが発生させています。
type Item struct { /* 省略 */ }
func main() {
db, err := open()
if err != nil {
log.Fatalln("DB Open Error:", err)
return
}
defer db.Close()
if err := queryTimeout(db); err != nil {
log.Fatalln("Query Timeout Error:", err)
return
}
}
func open() (*sql.DB, error) { /* 省略 */}
func queryTimeout(db *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
defer cancel()
_, err := db.QueryContext(ctx, "SELECT id, name, price, description FROM items")
return err
}
実行後のコンソールログ
2022/11/30 19:19:29 Query Timeout Error: context deadline exceeded
7. 最後に
sijms/go-ora
を使って基本的なDB処理を実装してみたところ、簡単なDB処理は問題なく動かせることを確認できました。
sijms/go-ora
を実際に使ってみた感想は、OR Mapperが使えないのでどうしても実装量が多くなったり、複雑化したりしてしまうという印象です。
ただ、他のDriverに比べて圧倒的に導入が簡単なので、複雑なDB操作をしないのであればsijms/go-ora
を利用したいなと思いました。
また、フェイルオーバー(RAC, Data Guard)への対応方法は未調査のため、今後調べてみたいと思います。