概要
CSV出力って結構いろんなシステムに機能として搭載されてますよね。
そんなCSVですが、あるサービスやシステムからCSVファイルを出力して、全く別のシステムやサービスのDBにその情報を保存しておきたいなんてケースがあるんじゃないでしょうか。
GoでCSVを扱うには標準ライブラリのencoding/csvがあります。
CSVの内容をDBに保存するにあたって構造体にマッピングできた方が処理が楽なのですが、encoding/csvではマッピングに手間がかかるため、今回はサードパーティのライブラリとしてcsvutilを使ってDBに保存してみます。
CSVを扱うライブラリ
ちなみにGoとCSVで検索してヒットするライブラリはgocsvとcsvutilが多数だと思います。
今回csvutilを選んだのには、決定的な理由があります。
それは CSVで値なしのとき、DBにnullとして登録できる という点です。
gocsvではCSVに値がないものを、Goの構造体にマッピングすると、ポインタ型でも0
がセットされてしまいます。
例えば*int型
にCSVの値なしをマッピングすると、nil
ではなく0
がセットされます。
これだと、CSV側で元々値が0
だったのか、値がなくてGoのゼロ値として0
になったのかがわかりません。
そんなわけで元々のCSVに値がない場合、DBには0
ではなくNULL
で登録されて欲しいので、Goでマッピングした時にnilだったらNULLとしてDBに登録するためにcsvutilを選んでいます。
gocsv
csvutil
ちなみにのちなみにcsvutilのGithubのREADMEにはcsvutilとgocsvのパフォーマンスの差がベンチマークの結果によって示されています。
パフォーマンス的にもcsvutilがよさそうですね。
少しだけ補足
JavaScriptではundefined
やnull
で値が未設定であることを判別すると思いますが、
Goでは、ポインタ型がnil
かどうかで未設定であることを判別します。
この記事が参考になりました。
やってみた
とりあえず最低限の環境構築をします。
環境構築用ファイル
version: '3.9'
services:
app:
image: golang:1.20.7
ports:
- 8080:8080
volumes:
- .:/app
tty: true
working_dir: /app
db:
image: mysql:8.0
container_name: mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test
MYSQL_USER: test
MYSQL_PASSWORD: test
TZ: Asia/Tokyo
ports:
- 3306:3306
volumes:
- ./docker/mysql/initdb.d:/docker-entrypoint-initdb.d
CREATE TABLE user (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100),
age INT,
memo TEXT,
PRIMARY KEY (id)
);
一応、gocsvとcsvutilでそれぞれ結果がどうなるか確認してみます。
今回CSVのサンプル値はコードに埋め込みますが、実際にはファイルから読み込むことになるため、その時はShiftJISからutf-8へ文字コードの変換が必要だったりします。
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/gocarina/gocsv"
"github.com/jszwec/csvutil"
)
type user struct {
Name string `csv:"name"`
Age *int `csv:"age"`
}
// CSVサンプル値
var csvInput = []byte(`
name,age
jack,26
john,,
`)
func main() {
// DB設定
db, err := sql.Open("mysql", "test:test@tcp(db:3306)/test")
if err != nil {
fmt.Println("DB Connection Error: ", err)
return
}
defer db.Close()
// DB接続確認
if err = db.Ping(); err != nil {
fmt.Println("Ping Error: ", err)
return
}
// csvutilで登録
csvutilUsers := []user{}
if err := csvutil.Unmarshal(csvInput, &csvutilUsers); err != nil {
fmt.Println("csvutil Unmarshal Error:", err)
}
insertUser(db, csvutilUsers, "csvutil")
// gocsvで登録
gocsvUsers := []user{}
if err := gocsv.UnmarshalBytes(csvInput, &gocsvUsers); err != nil {
fmt.Println("gocsv Unmarshal Error:", err)
}
insertUser(db, gocsvUsers, "gocsv")
}
// DBにユーザー登録
func insertUser(db *sql.DB, users []user, lib string) {
for _, u := range users {
age := sql.NullInt16{}
if u.Age != nil {
age = sql.NullInt16{Int16: int16(*u.Age), Valid: true}
}
query := "INSERT INTO user (name, age, memo) VALUES (?, ?, ?)"
_, err := db.Exec(query, u.Name, age, lib)
if err != nil {
fmt.Println("INSERT Error: ", err)
}
}
}
同じ構造体、同じCSVサンプル値に対して、gocsvとcsvutilでそれぞれUnmarshalしてDBに登録した結果、こうなりました。
mysql> select * from user;
+----+------+------+---------+
| id | name | age | memo |
+----+------+------+---------+
| 1 | jack | 26 | csvutil |
| 2 | john | NULL | csvutil |
| 3 | jack | 26 | gocsv |
| 4 | john | 0 | gocsv |
+----+------+------+---------+
csvutilではCSVに値がないときにNULLが登録され、
gocsvではCSVに値がないときに0が登録されています。
所感
ライブラリを読み込んだわけではないのでひょっとするとgocsvでもNULLで登録ができるのかもしれません。
ただパッと使ってみた感じではcsvutilが簡単にいろんなことができそうな印象です。