Open2

SQLite + GORM + zap = ♥

SpiegelSpiegel

いきなりサンプルコードから。

sample.go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "os"
    "sync"

    "github.com/glebarez/sqlite"
    "github.com/goark/errs"
    "go.uber.org/zap"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "moul.io/zapgorm2"
)

type Planet struct {
    gorm.Model
    Name     string
    Mass     float64
    Distance float64
}

var planets = []Planet{
    {Name: "Mercury", Mass: 0.055, Distance: .4},
    {Name: "Venus", Mass: 0.815, Distance: .7},
    {Name: "Earth", Mass: 1.0, Distance: 1.0},
    {Name: "Mars", Mass: 0.107, Distance: 1.5},
}

const dbfile = "./sqlite.db"

func main() {
    // initialize logger
    zlogger := zap.NewExample()
    defer zlogger.Sync()
    glogger := zapgorm2.New(zlogger)
    glogger.SetAsDefault()

    // open SQLite file
    db, err := gorm.Open(sqlite.Open(dbfile), &gorm.Config{Logger: glogger.LogMode(logger.Info)})
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }

    // migration
    db.WithContext(context.TODO()).AutoMigrate(&Planet{})

    // create data
    var wg sync.WaitGroup
    var errlist *errs.Errors
    for _, p := range planets {
        p := p
        wg.Add(1)
        go func() {
            defer wg.Done()
            if err := db.WithContext(context.TODO()).Transaction(func(tx *gorm.DB) error {
                if t := tx.Create(&p); t.Error != nil {
                    fmt.Fprintln(os.Stderr, t.Error)
                    return tx.Error
                }
                return nil
            }); err != nil {
                errlist.Add(errs.Wrap(err, errs.WithContext("data", p)))
            }
        }()
    }
    wg.Wait()
    err = errlist.ErrorOrNil()
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }

    // read
    data := []Planet{}
    if tx := db.WithContext(context.TODO()).Order("mass asc, id asc").Find(&data); tx.Error != nil {
        fmt.Fprintln(os.Stderr, tx.Error)
        return
    }
    if err := json.NewEncoder(os.Stdout).Encode(data); err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }

    // remove file
    if err := os.Remove(dbfile); err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
}

手順としては

  1. zap ロガーを生成。 GORM 用に decorate する
  2. SQLite ファイルをオープン(ファイルがない場合は生成)。ロガーとして 1 で生成したものをセットする
  3. Planet 型構造体を使ってテーブルを作成
  4. 作成したテーブルにデータを追加する。平行処理で別々の goroutine からアクセスしているのがポイント
  5. 格納したデータを mass 値でソートして取得する
  6. 取得したデータを JSON 形式にエンコードして出力
  7. 使い終わった SQLite ファイルを削除

といった感じ。

SpiegelSpiegel

これを実行した結果が以下の通り。

$ go run sample.go | jq .
{
  "level": "debug",
  "msg": "trace",
  "elapsed": "84.469µs",
  "rows": -1,
  "sql": "SELECT count(*) FROM sqlite_master WHERE type='table' AND name=\"planets\""
}
{
  "level": "debug",
  "msg": "trace",
  "elapsed": "8.046015ms",
  "rows": 0,
  "sql": "CREATE TABLE `planets` (`id` integer,`created_at` datetime,`updated_at` datetime,`deleted_at` datetime,`name` text,`mass` real,`distance` real,PRIMARY KEY (`id`))"
}
{
  "level": "debug",
  "msg": "trace",
  "elapsed": "2.29142ms",
  "rows": 0,
  "sql": "CREATE INDEX `idx_planets_deleted_at` ON `planets`(`deleted_at`)"
}
{
  "level": "debug",
  "msg": "trace",
  "elapsed": "537.511µs",
  "rows": 1,
  "sql": "INSERT INTO `planets` (`created_at`,`updated_at`,`deleted_at`,`name`,`mass`,`distance`) VALUES (\"2023-07-02 12:52:44.81\",\"2023-07-02 12:52:44.81\",NULL,\"Earth\",1.000000,1.000000) RETURNING `id`"
}
{
  "level": "debug",
  "msg": "trace",
  "elapsed": "3.878395ms",
  "rows": 1,
  "sql": "INSERT INTO `planets` (`created_at`,`updated_at`,`deleted_at`,`name`,`mass`,`distance`) VALUES (\"2023-07-02 12:52:44.81\",\"2023-07-02 12:52:44.81\",NULL,\"Mars\",0.107000,1.500000) RETURNING `id`"
}
{
  "level": "debug",
  "msg": "trace",
  "elapsed": "9.780918ms",
  "rows": 1,
  "sql": "INSERT INTO `planets` (`created_at`,`updated_at`,`deleted_at`,`name`,`mass`,`distance`) VALUES (\"2023-07-02 12:52:44.81\",\"2023-07-02 12:52:44.81\",NULL,\"Venus\",0.815000,0.700000) RETURNING `id`"
}
{
  "level": "debug",
  "msg": "trace",
  "elapsed": "20.146606ms",
  "rows": 1,
  "sql": "INSERT INTO `planets` (`created_at`,`updated_at`,`deleted_at`,`name`,`mass`,`distance`) VALUES (\"2023-07-02 12:52:44.811\",\"2023-07-02 12:52:44.811\",NULL,\"Mercury\",0.055000,0.400000) RETURNING `id`"
}
{
  "level": "debug",
  "msg": "trace",
  "elapsed": "313.95µs",
  "rows": 4,
  "sql": "SELECT * FROM `planets` WHERE `planets`.`deleted_at` IS NULL ORDER BY mass asc, id asc"
}
[
  {
    "ID": 4,
    "CreatedAt": "2023-07-02T12:52:44.811046342+09:00",
    "UpdatedAt": "2023-07-02T12:52:44.811046342+09:00",
    "DeletedAt": null,
    "Name": "Mercury",
    "Mass": 0.055,
    "Distance": 0.4
  },
  {
    "ID": 2,
    "CreatedAt": "2023-07-02T12:52:44.810572761+09:00",
    "UpdatedAt": "2023-07-02T12:52:44.810572761+09:00",
    "DeletedAt": null,
    "Name": "Mars",
    "Mass": 0.107,
    "Distance": 1.5
  },
  {
    "ID": 3,
    "CreatedAt": "2023-07-02T12:52:44.810994925+09:00",
    "UpdatedAt": "2023-07-02T12:52:44.810994925+09:00",
    "DeletedAt": null,
    "Name": "Venus",
    "Mass": 0.815,
    "Distance": 0.7
  },
  {
    "ID": 1,
    "CreatedAt": "2023-07-02T12:52:44.810782185+09:00",
    "UpdatedAt": "2023-07-02T12:52:44.810782185+09:00",
    "DeletedAt": null,
    "Name": "Earth",
    "Mass": 1,
    "Distance": 1
  }
]

ID 値を見ると分かるように,入力データの順番に ID が振られているわけではない。平行処理だから当たり前だけど。まぁ,でも,これで充分使えることが分かったのでよしとしよう。