3
1

GoでCSVの内容をデータベースに保存する

Last updated at Posted at 2023-08-30

概要

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ではundefinednullで値が未設定であることを判別すると思いますが、
Goでは、ポインタ型がnilかどうかで未設定であることを判別します。
この記事が参考になりました。

やってみた

とりあえず最低限の環境構築をします。

環境構築用ファイル
docker-compose.yml
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
docker/mysql/initdb.d/01_create_table.sql
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へ文字コードの変換が必要だったりします。

main.go
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が簡単にいろんなことができそうな印象です。

3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1