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 init
、 go 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 時の時間を CreatedAt
、 UpdatedAt
という構造体のフィールドと動悸します。
デフォルトの規約に従えば、コード量は激減します。
ただし、もし規約に従うことができない理由があればこれを設定で上書きすることも可能です。
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
}
作成時・更新時の時間計測 ( 秒、ミリ秒、ナノ秒 )
CreatedAt
、 UpdatedAt
を使って作成時、更新時を計測できます。
上記のデフォルトのフィールド名を変更したい場合には、フィールドのタグに autoCreateTime
、 autoUpdateTime
を設定します。
ミリ秒、ナノ秒を保存したい場合には、タグに追加情報を付与した上で、フィールドの型を 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
}
ひとこと
今回は冒頭にも書いたとおり、導入と触りまでです。
次回は登録操作に踏み込んで行きます。
ディスカッション
コメント一覧
まだ、コメントがありません