Go言語で簡単にDBアクセスを実現!GORMを使ってORMを体験しよう-導入編

はじめに

Go 言語上で SQL を発行せずに DB アクセスを実現するための GORM というライブラリを触ってみます。

GORM は Go 言語上で SQL を発行せずに DB アクセスを実現するための O/R マッパー ライブラリです。
特徴として、アソシエーション機能、イベントフック、トランザクションなどが提供されています。
本記事では、GORM の基本的な使い方を紹介し、次回以降に登録・更新・削除操作に踏み込んで行きたいと思っています。

検証環境

$ go version
go version go1.20.2 darwin/arm64

$ grep 'gorm.io/gorm' go.mod
    gorm.io/gorm v1.24.6 // indirect

GORM とは?

名前から推測できる通り、GO 言語で使える O/R マッパー ライブラリです。

特徴

  • O/R マッパー として提供されている他言語のライブラリと同じような機能が提供されています。
  • アソシエーション機能 ( Has one、Has many、Belongs to、Many to many、Polymorphism などなど )
  • イベントフック ( 利用できるトリガは Before、After create、S;ve、Update、Delete、Find など )
  • トランザション、ネスティッドトランザクション、セーブポイントにロールバック、コミット
  • プリペアドステートメント
  • バッチインサート
  • SQL ビルド
  • Upsert、Lock、インデックスヒント
  • マイグレーション
  • ロギング

インストール方法

go mod initgo mod tidy を実行し、ディレクトリ配下に go.mod ファイルが存在している状態であれば、以下のコマンドを実行するだけでインストールできます。

( ここでは SQLite を使用する場合を例に上げています。
)

$ go get -u gorm.io/gorm

$ go get -u gorm.io/driver/sqlite

早速使ってみる

先にあげた「インストール方法」の手順を事前に実施しておきましょう。

$ go mod init gorm-example
go: creating new go.mod: module gorm-example

$ go mod tidy
go: warning: "all" matched no packages

$ go get -u gorm.io/gorm
go: downloading gorm.io/gorm v1.24.6
go: downloading github.com/jinzhu/now v1.1.5
go: added github.com/jinzhu/inflection v1.0.0
go: added github.com/jinzhu/now v1.1.5
go: added gorm.io/gorm v1.24.6

$ go get -u gorm.io/driver/sqlite
go: downloading gorm.io/driver/sqlite v1.4.4
go: downloading github.com/mattn/go-sqlite3 v1.14.15
go: downloading github.com/mattn/go-sqlite3 v1.14.16
go: added github.com/mattn/go-sqlite3 v1.14.16
go: added gorm.io/driver/sqlite v1.4.4

以下のような簡単なコードを用意します。

package main

import (
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type Product struct {
    gorm.Model
    Code  string
    Price uint
}

func main() {
    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }

    // Migrate the schema
    db.AutoMigrate(&Product{})

    // Create
    db.Create(&Product{Code: "D42", Price: 100})

    // Read
    var product Product
    db.First(&product, 1)                 // find product with integer primary key
    db.First(&product, "code = ?", "D42") // find product with code D42

    // Update - update product's price to 200
    db.Model(&product).Update("Price", 200)
    // Update - update multiple fields
    db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields
    db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

    // Delete - delete product
    db.Delete(&product, 1)
}

実行すると、カレントディレクトリに test.db というファイルが生成されます。

$ go run main.go

$ ls test.db
test.db

コマンドラインから sqlite3 コマンドを使ってアクセスしてみます。

$ sqlite3 test.db

sqlite> .table
products

sqlite> select * from products;
id  created_at                        updated_at                        deleted_at                        code  price
--  --------------------------------  --------------------------------  --------------------------------  ----  -----
1   2023-04-10 10:32:28.223178+09:00  2023-04-10 10:32:28.225196+09:00  2023-04-10 10:32:28.225735+09:00  F42   200
sqlite>

sqlite> .schema products
CREATE TABLE `products` (`id` integer,`created_at` datetime,`updated_at` datetime,`deleted_at` datetime,`code` text,`price` integer,PRIMARY KEY (`id`));
CREATE INDEX `idx_products_deleted_at` ON `products`(`deleted_at`);
  • products というテーブルが作成されています。
  • レコードは 1 件追加されています。
  • テーブルは、コード上に登場する Product という構造体にフィールドである code、price というカラムの他に、id、created_at、updated_at、deleted_at というカラムを持っています。

実際に発行されている SQL を確認したときは、先程のコードに 1 行処理を追記します。

package main

import (
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type Product struct {
    gorm.Model
    Code  string
    Price uint
}

func main() {
    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }

    // ***この一文を追加する***
    db = db.Debug()

    // Migrate the schema
    db.AutoMigrate(&Product{})

    // Create
    db.Create(&Product{Code: "D42", Price: 100})

    // Read
    var product Product
    db.First(&product, 1)                 // find product with integer primary key
    db.First(&product, "code = ?", "D42") // find product with code D42

    // Update - update product's price to 200
    db.Model(&product).Update("Price", 200)
    // Update - update multiple fields
    db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields
    db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

    // Delete - delete product
    db.Delete(&product, 1)
}

ここまでで駆け足となりましたが、以下のような操作を実装できました。

  • 定義した構造体を元に DB テーブルを簡単に作成
  • 構造体を使って INSERT、SELECT、UPDATE、DELETE を実行

モデルの定義

モデルは Go 言語で提供される単純な構造体であることがわかります。

また、これらの構造体は Scanner や Valuer というインターフェイスで提供さているメソッドを実装したものであることが期待されています。

Conventions

GORM では "converntion over configuration" という思想が好まれています。

「設定よりも規約」という Rails でも期待されている考え方です。

デフォルトでは、GORM はID というフィールドを主キーとして扱いますし、テーブル名は snake_cases のように複数形の名前として扱います。
テーブルカラム名は snake_case のように小文字のスネークケースを期待しますし、 insert/update 時の時間を CreatedAtUpdatedAt という構造体のフィールドと動悸します。

デフォルトの規約に従えば、コード量は激減します。
ただし、もし規約に従うことができない理由があればこれを設定で上書きすることも可能です。

gorm.Model

gorm.Model という構造体が用意されています。

以下のようなフィールドを内包しています。

  • ID
  • CreatedAt
  • UpdatedAt
  • DaletedAt
type Model struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt DeletedAt `gorm:"index"`
}

多くのテーブルは上記のフィールドを共通で所有する事が多いため、自作した構造体に対して Go 言語の Emmbedded Struct という機能を使って埋め込むことで、フィールド定義のコードを省略できます。

type Product struct {
    gorm.Model
    Code  string
    Price uint
}

ここでは、 Product 構造体は以下のフィールドを保持することとなります。

  • ID
  • CreatedAt
  • UpdatedAt
  • DeletedAt
  • Code
  • Price

モデルの拡張機能

フィールドレベルの権限

大文字で始まる構造体フィールドは CURD のすべての権限を持つこととなります。

GORM は更に権限を絞り込む機能を提供してくれます。
構造体のフィールドに対して タグを設定するのです。
フィールドを「読み取り専用」「書き込み専用」「作成専用」「更新専用」さらには「無視させる」といった設定が可能です。

( 注意 ) 「無視する」設定としたフィールドはマイグレーション時にも無視されてしれてしまいます。

type User struct {
    Name string `gorm:"<-:create"`          // allow read and create
    Name string `gorm:"<-:update"`          // allow read and update
    Name string `gorm:"<-"`                 // allow read and write (create and update)
    Name string `gorm:"<-:false"`           // allow read, disable write permission
    Name string `gorm:"->"`                 // readonly (disable write permission unless it configured)
    Name string `gorm:"->;<-:create"`       // allow read and create
    Name string `gorm:"->:false;<-:create"` // createonly (disabled read from db)
    Name string `gorm:"-"`                  // ignore this field when write and read with struct
    Name string `gorm:"-:all"`              // ignore this field when write, read and migrate with struct
    Name string `gorm:"-:migration"`        // ignore this field when migrate with struct
}

作成時・更新時の時間計測 ( 秒、ミリ秒、ナノ秒 )

CreatedAtUpdatedAt を使って作成時、更新時を計測できます。

上記のデフォルトのフィールド名を変更したい場合には、フィールドのタグに autoCreateTimeautoUpdateTime を設定します。

ミリ秒、ナノ秒を保存したい場合には、タグに追加情報を付与した上で、フィールドの型を time.Time型から int64 に変更します。

type User struct {
    CreatedAt time.Time // Set to current time if it is zero on creating
    UpdatedAt int       // Set to current unix seconds on updating or if it is zero on creating
    Updated   int64     `gorm:"autoUpdateTime:nano"`  // Use unix nano seconds as updating time
    Updated   int64     `gorm:"autoUpdateTime:milli"` // Use unix milli seconds as updating time
    Created   int64     `gorm:"autoCreateTime"`       // Use unix seconds as creating time
}

「埋め込み」された構造体

前述の通り、 gorm.Model 構造体を埋め込んだ構造体の中身は以下のようになります。

type User struct {
    gorm.Model
    Name string
}

// equals
type User struct {
    ID        uint `gorm:"primaryKey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt gorm.DeletedAt `gorm:"index"`
    Name      string
}

gorm.Model 構造体のように、他のモデルに共通で登場するフィールドを他の構造体から「埋め込み」したい場合には embedded タグを付与します。

type Author struct {
    Name  string
    Email string
}

type Blog struct {
    ID      int
    Author  Author `gorm:"embedded"`
    Upvotes int32
}

// equals
type Blog struct {
    ID      int64
    Name    string
    Email   string
    Upvotes int32
}

更に、「埋め込み」フィールドに任意のプレフィックスを付与したい場合には emmbeddedPrefix タグを使用します。


type Blog struct {
    ID      int
    Author  Author `gorm:"embedded;embeddedPrefix:author_"`
    Upvotes int32
}

// equals
type Blog struct {
    ID          int64
    AuthorName  string
    AuthorEmail string
    Upvotes     int32
}

ひとこと

今回は冒頭にも書いたとおり、導入と触りまでです。
次回は登録操作に踏み込んで行きます。

Posted by genzouw