はじめに
GoでWebアプリケーションを開発する際、データベースとのやり取りをスムーズにするためにORM(Object Relational Mapper)を導入することが一般的です。この記事では、Go向けの新しいORMツールである ent を導入する方法を説明しながら、entとは何か、なぜ選んだのかについても詳しく解説します。
なお、以下の記事の続きです。
entとは?
ent は、Facebook(現Meta)が開発したGo向けのエンティティフレームワークです。他のORMと比較して次のような特徴を持っています:
特徴
-
コード生成ベースのスキーマ管理:
- スキーマ(データベース構造)をGoのコードで定義します。
- 定義に基づいて自動的に型安全なクエリメソッドが生成されます。
-
型安全なクエリ:
- クエリがGoの型に依存するため、開発中にエラーを早期に発見できます。
-
柔軟な設計:
- リレーションやユニーク制約など、複雑なデータ構造を直感的に扱えます。
なぜentを選んだのか?
Goでの開発において、RailsのActive Recordに慣れている経験がentの採用を後押ししました。entとActive Recordにはいくつか共通点があり、移行時の学習コストが低いと判断したからです。
Active Record経験が活きるポイント
-
スキーマ定義の親しみやすさ:
- Active Recordではマイグレーションファイルを通じてスキーマを管理します。一方で、entではGoコードでスキーマを定義しますが、設計思想が似ており、フィールドやリレーションの設定も直感的に行えます。
-
オブジェクト指向的なデータ操作:
- Active Recordではモデルを介してデータベース操作を行いますが、entも型安全なクライアントとメソッドを提供しており、同様の感覚でクエリを記述できます。
-
リレーションのサポート:
- Active Recordと同様に、entもリレーション(1対1、1対多、多対多)を簡単に扱えるため、既存の知識を活かしやすいです。
導入手順
以下に、新聞コラムWebアプリにentを導入し、データベースとの接続とスキーマ管理を行う手順を説明します。
1. entのインストール
コンテナを起動しshellへ入る
docker-compose run backend sh
Goコマンドを実行してEntをインストール
# go get entgo.io/ent/cmd/ent
2. スキーマの作成とプロジェクトの初期化
# go run -mod=mod entgo.io/ent/cmd/ent new Article
# go run -mod=mod entgo.io/ent/cmd/ent new Newspaper
すると新たにent
ディレクトリが作成されます。また、ent/schema
ディレクトリには、 エンティティスキーマが作成されています。
作成されたent/schema/newspaper.go
とent/schema/article.go
に変更を加えます
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"time"
)
// Newspaper holds the schema definition for the Newspaper entity.
type Newspaper struct {
ent.Schema
}
// Fields of the Newspaper.
func (Newspaper) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty().
Unique(),
field.String("column_name").
NotEmpty(),
field.Time("created_at").
Default(time.Now),
field.Time("updated_at").
Default(time.Now).
UpdateDefault(time.Now),
}
}
// Edges of the Newspaper.
func (Newspaper) Edges() []ent.Edge {
return []ent.Edge{
edge.To("articles", Article.Type),
}
}
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"time"
)
// Article holds the schema definition for the Article entity.
type Article struct {
ent.Schema
}
// Fields of the Article.
func (Article) Fields() []ent.Field {
return []ent.Field{
field.Text("content").
NotEmpty(),
field.Int("year").
Positive(),
field.Int("month").
Range(1, 12),
field.Int("day").
Range(1, 31),
field.Time("created_at").
Default(time.Now),
field.Time("updated_at").
Default(time.Now).
UpdateDefault(time.Now),
}
}
// Edges of the Article.
func (Article) Edges() []ent.Edge {
return []ent.Edge{
edge.From("newspaper", Newspaper.Type).
Ref("articles").
Unique(),
}
}
再度コンテナのshellに入る
docker-compose run backend sh
go generate
コマンドを実行
# go generate ./ent
go generate ./ent
を実行すると、Entの自動コード生成ツールが起動し、schema
パッケージで定義したスキーマを使用して、データベースと対話するためのGoコードを生成されます。
そして、自動生成されたパッケージに必要なモジュールを取得するために、以下のコマンドを実行します。
docker-compose run backend sh
# go mod tidy
3. PostgreSQLとの接続
公式サイトには以下のように記述されています。
https://entgo.io/ja/docs/getting-started/
package main
import (
"context"
"log"
"entdemo/ent"
_ "github.com/lib/pq"
)
func main() {
client, err := ent.Open("postgres","host=<host> port=<port> user=<user> dbname=<database> password=<pass>")
if err != nil {
log.Fatalf("failed opening connection to postgres: %v", err)
}
defer client.Close()
// Run the auto migration tool.
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
こちらを参考にして、main.go
の内容を変更します。
package main
import (
"github.com/gin-gonic/gin"
"context"
"log"
"os"
"github.com/joho/godotenv"
"github.com/jijimama/newspaper-app/ent"
_ "github.com/lib/pq"
)
func main() {
router := gin.Default()
// .envファイルを読み込む
err := godotenv.Load()
if err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
// 環境変数から取得
dbHost := os.Getenv("DB_HOST")
dbPort := os.Getenv("DB_PORT")
dbUser := os.Getenv("DB_USER")
dbName := os.Getenv("DB_NAME")
dbPassword := os.Getenv("DB_PASSWORD")
// 接続文字列を作成
dsn := "host=" + dbHost + " port=" + dbPort + " user=" + dbUser + " dbname=" + dbName + " password=" + dbPassword + " sslmode=disable"
//PostgreSQLに接続
client, err := ent.Open("postgres", dsn)
if err != nil {
log.Fatalf("failed opening connection to postgres: %v", err)
}
defer client.Close()
// Run the auto migration tool.
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
router.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello, World!",
})
})
router.Run(":8080")
}
bakend配下に.envファイルを作成し、以下のようにしました。
DB_HOST=db
DB_PORT=5432
DB_USER=postgres
DB_NAME=db
DB_PASSWORD=password
4. DBの確認
コンテナを起動し、dbのbashに入ります。
docker-compose up backend -d
docker compose exec db bash
そして、DBに接続します。
psql -U postgres -d db
そして、テーブルを確認します。
db=# \dt
List of relations
Schema | Name | Type | Owner
--------+------------+-------+----------
public | articles | table | postgres
public | newspapers | table | postgres
(2 rows)
db=# \d articles
Table "public.articles"
Column | Type | Collation | Nullable | Default
--------------------+--------------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
content | text | | not null |
year | bigint | | not null |
month | bigint | | not null |
day | bigint | | not null |
created_at | timestamp with time zone | | not null |
updated_at | timestamp with time zone | | not null |
newspaper_articles | bigint | | |
Indexes:
"articles_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"articles_newspapers_articles" FOREIGN KEY (newspaper_articles) REFERENCES newspapers(id) ON DELETE SET NULL
無事に、テーブルが生成されていることを確認できました。
遭遇したエラー
column(コラム)テーブルを作成しようとしました。
# go run -mod=mod entgo.io/ent/cmd/ent new Column
すると、
ent/ent.go:78:11: column.Table undefined (type string has no field or method Table)
というエラーになりました。予約語であることを失念していました。
参考文献
ent リファレンスマニュアル
https://qiita.com/takakou/items/4f8cd3686c7ec84c141d