sijms/go-oraを使ってOracleDBを操作してみる (Golang)

Go言語を学び始めて1ヶ月の私が、Oracle DBのCRUD処理を実装する必要があったので、記事を書いてみようと思います。

# 1. はじめに

## 1.1. 読んでみてほしい対象者

* Go言語でOracle DB接続を実装したい人
* OR Mapperを利用できないため、簡易的にDB処理を実装したい人

## 1.2 利用するOracle Driver

今回利用するOracle Driverは`sijms/go-ora`です。

https://github.com/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で環境構築が大変な話は下記の記事が参考になると思います。
      * [Oracleを使うGoアプリのコンテナ化メモ（準備編）](https://qiita.com/yuuman/items/09f6d3e43e96de0a5096)
      * [Oracleを使うGoアプリのコンテナ化メモ（挫折編）](https://qiita.com/yuuman/items/9f988a9cf4b6dcf4a969)
  * コンテナのイメージサイズ軽量化
    * 他のOracle Driverの場合、`Oracle Instant Client`が必要なため、コンテナ化するとイメージサイズが大きくなってしまいます。
    * 例 : 「sijms/go-ora」と「[mattn/go-oci8](https://github.com/mattn/go-oci8)」のコンテナイメージサイズの比較
      * `sijms/go-ora`: 13.9MB
      * `mattn/go-oci8`: 1.5GB
* デメリット
  * OR Mapperが使えない。
    * [xorm](https://xorm.io/)や[gorm](https://gorm.io/)で`sijms/go-ora`は対応していないです。(2022/12/03 時点)

## 1.3. 環境情報

|  環境  |                                         バージョン                                         |
| ------ | ------------------------------------------------------------------------------------------ |
| OS     | Windows10 Home 21H2                                                                        |
| Golang | 19.1                                                                                       |
| Oracle | [19c(19.3.0)](https://www.oracle.com/database/technologies/oracle19c-linux-downloads.html) |

## 1.4. 動作確認するテーブル情報

下記のような商品を模したテーブルを作成して、OracleDBの動作確認をしてみます。

```sql
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](https://pkg.go.dev/database/sql)で用意された型(NullInt32, NullString, etc..)を利用しましょう。  
(database/sqlの型を利用しないと、nullをSELECTした時などにエラーが起きます。)

```golang
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()`関数があるので、これを利用すると実装が楽です。

```golang
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を実行します。

```golang
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結果

```bash
ID  NAME            PRICE DESCRIPTION
--- ---------- ---------- --------------------
001 pen               100 Color is black.
```

## 3.3. Select

sql.DBの`Query()`関数を利用して、SQLを実行します。  
Entityへのマッピング処理は、sql.Rowsの`Next()`, `Scan()`関数を使います。

```golang
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
}
```

実行後のコンソールログ

```bash
2022/11/30 18:07:21 [{001 pen 100 {Color is black. true}}]
```

## 3.4. Update

sql.DBの`Exec()`関数を利用して、SQLを実行します。  
更新された行数を取得したい場合は、`RowsAffected()`関数を利用します。

```golang
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結果

```bash
ID  NAME            PRICE DESCRIPTION
--- ---------- ---------- --------------------
001 pen               200 Color is black.
```

## 3.5. Delete

sql.DBの`Exec()`関数を利用して、SQLを実行します。

```golang
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結果

```bash
ID  NAME            PRICE DESCRIPTION
--- ---------- ---------- --------------------

```

# 4. DDL編

## 4.1. Create Table

sql.DBの`Exec()`関数を利用して、SQLを実行します。

```golang
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結果

```bash
TABLE_NAME
--------------------------------------------------------------------------------
ITEMS
```

## 4.2. Drop Table

sql.DBの`Exec()`関数を利用して、SQLを実行します。

```golang
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結果

```bash
TABLE_NAME
--------------------------------------------------------------------------------

```

# 5. DCL編

## 5.1. Rollback

トランザクションの開始はsql.DB`Begin()`関数を使い、返り値のsql.Txでトランザクション制御を行います。  

3, 4章では、sql.DBの`Query()`や`Exec()`を利用していましたが、  
トランザクション制御する場合は、sql.Txの`Query()`や`Exec()`を利用する必要があります。

ロールバックはsql.Txの`Rollback()`関数を使います。

```golang
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結果

```bash
ID  NAME            PRICE DESCRIPTION
--- ---------- ---------- --------------------

```

## 5.2. Commit

トランザクションの開始はsql.DB`Begin()`関数を使い、コミットはsql.Txの`Commit()`関数を使います。


```golang
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結果

```bash
ID  NAME            PRICE DESCRIPTION
--- ---------- ---------- --------------------
001 pen               100
```

# 6. Timeout編

## 6.1 Query Timeout

contextの`WithTimeout()`で指定した時間を経過後に、クエリの実行を中断させることができます。  
contextを設定して、クエリを実行するためには、`QueryContext()` or `ExecContext()`関数を使います。

サンプルでは、タイムアウト時間を極限まで小さくすることで、タイムアウトが発生させています。

```golang
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
}
```

実行後のコンソールログ

```bash
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)への対応方法は未調査のため、今後調べてみたいと思います。

# 参考文献

* <https://github.com/sijms/go-ora>
* <https://github.com/mattn/go-oci8>
* [database/sqlによるデータベース操作](https://www.wakuwakubank.com/posts/869-go-database-sql/)
* [SQL Query Timeout With Golang Context](https://www.jajaldoang.com/post/sql-query-timeout-with-golang-context/)
* [Oracleを使うGoアプリのコンテナ化メモ（準備編）](https://qiita.com/yuuman/items/09f6d3e43e96de0a5096)
* [Oracleを使うGoアプリのコンテナ化メモ（挫折編）](https://qiita.com/yuuman/items/9f988a9cf4b6dcf4a969)
* [Oracleを使うGoアプリのコンテナ化メモ（最終回）](https://qiita.com/yuuman/items/dab16bdf5777b096f62b)