GolangでORMを使うなら ent. がおすすめ!
1. はじめに
「Active Recordみたいな使用感のGolang製のORMが無いかなー?」と思って色々なORMを触っている中で、おすすめのORM「ent.」見つけたので備忘録+布教も兼ねて本記事を残そうと思います。
--- 2024/07/22 追記 ---
本記事で割愛していたVisualize the Schemaに関する記事を投稿しました!
スキーマの可視化に興味のある方は覗いてみてください!!
※ リンク先のURLが間違っていたので修正しました///
--- 2024/07/22 追記 ---
atlasを用いたバージョン管理型マイグレーションに関する記事を投稿しました!
ent.のチュートリアルを行った後に、atlasどうやって使うんだ?って思われた方は読んでみて下さいー!!
2. ent.って何?
Metaが開発しているGolang製のORMです。
近年、githubのスターも伸びており、公式のドキュメントも充実しているので安心感があります。
また、RailsやDjangoでの開発経験がある方には、以下の特徴があるため非常に馴染みやすいと思っています。
- 定義したschemaからテーブルが作れる
- Atlasを利用することで、マイグレーションのバージョンを管理できる
- CRUD APIによる操作が可能
3. 開発環境
以下に、筆者の開発環境と成果物のリポジトリを記載します。
- golang 1.21.4
- docker 4.16.2
- postgresql 16.1
- ent. v0.12.5
リポジトリはこちらになります。
4. ent.のQuick Introduction
公式にQuick Introductionがありますが、日本語訳が無く、分かりにくい箇所もあるので、以下にテーブル定義等と合わせて使用方法を記載します。
4-1 開発環境の準備
Quick Introductionを実行するためのディレクトリenddemo
とgo.mod
を作成します。
mkdir entdemo
cd entdemo
go mod init entdemo
4-2 データベースの準備
スキーマからテーブルを作成するためのDBを準備します。
4-2-1. docker-composeの作成
DBはdocker-composeで準備するので、以下のようにdocker-compose.yml
を作成します。
touch docker-compose.yml
version: "3.8"
services:
postgres:
image: postgres:16.1-alpine
container_name: postgres
environment:
POSTGRES_DB: entdemo
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- ./data/db_data/volume:/var/lib/postgresql/data
準備ができたら、コンテナを起動させます。
docker-compose up -d
4-3. はじめてのスキーマ作成
4-3-1. 雛形の作成
entdemoのrootディレクトリで、以下のコマンドを実行することで、スキーマの雛形が作成できます。
go run -mod=mod entgo.io/ent/cmd/ent new User
この時、ent/
が作成されent/schema/
の中に、以下の内容でuser.go
というファイルが生成されます。
package schema
import "entgo.io/ent"
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return nil
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return nil
}
4-3-2. フィールドの追加
では、User
にage
とname
というフィールドを以下のように追加しましょう。
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
4-3-3. アセットの作成
以下のコマンドで、スキーマからアセットの作成を行います。
go generate ./ent
このコマンドを実行することで、以下のアセットがent/
に自動生成されます。
ent
├── client.go
├── ent.go
├── enttest
│ └── enttest.go
├── hook
│ └── hook.go
├── migrate
│ ├── migrate.go
│ └── schema.go
├── mutation.go
├── predicate
│ └── predicate.go
├── runtime
│ └── runtime.go
├── runtime.go
├── tx.go
├── user
│ ├── user.go
│ └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go
作成されたアセットは、以下の用途で利用されます。
-
tx.go
とclinet.go
- Client と Tx オブジェクトはグラフとのやり取りに使用されます。
-
user_create.go
とuser_delete.go
とuser_query.go
とuser_update.go
- 各スキーマ型の CRUD ビルダー。詳細はCRUDを参照して下さい。
-
user/user.go
とuser/where.go
とuser.go
- 各スキーマタイプのエンティティオブジェクト (Go struct)
-
predicate/predicate.go
- ビルダーとの相互作用に使用される定数と述語を含むパッケージ。
-
migrate/migrate.go
とmigrate/schema.go
- SQLの migrate パッケージ。 詳細はMigrationを参照してください。
-
hook/hook.go
- mutationミドルウェアを追加するためのhook パッケージ。 詳細はHooksを参照してください。
-
enttest/enttest.go
- ユニットテストで利用できるパッケージ。詳細は Testing を参照して下さい。
-
mutation.go
- スキーマへのCRUD操作が記載されている。
あとは、自動生成されたパッケージに必要なモジュールを取得するために、以下のコマンドを実行して準備は完了です!
go mod tidy
4-4. 初めてのエンティティ作成
4-4-1. マイグレーション
それでは、Client
を作成してマイグレーションを行いましょう。
entdemo
のルートディレクトリにstart.go
を作成して、以下の内容を記述して下さい。
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)
}
}
作成したら、以下のコマンドを実行して不足しているモジュールを取得して下さい。
go mod tidy
最後に、以下のコマンドでstart.go
を実行してマイグレーションを完了させましょう。
go run start.go
4-4-2. 作成されたテーブルの確認
では、実際にマイグレーションが完了しているか確認しましょう。
今回はコンテナ上にpostgresqlを立ち上げているので、以下のように確認することができます。
# postgresqlに接続
docker exec -it postgres psql -U postgres entdemo
psql (16.1)
Type "help" for help.
# テーブル一覧
entdemo=# \dt;
List of relations
Schema | Name | Type | Owner
--------+-------+-------+----------
public | users | table | postgres
(1 row)
# レコードの確認
entdemo=# select * from users;
id | age | name
----+-----+------
(0 rows)
# テーブル構造の確認
entdemo=# \d users;
Table "public.users"
Column | Type | Collation | Nullable | Default
--------+-------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
age | bigint | | not null |
name | character varying | | not null | 'unknown'::character varying
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
4-4-3. Userエンティティの作成
マイグレーションが完了したら、実際にUserエンティティを作成してみましょう。
以下のCreateUser
を、start.go
で実行することでUserエンティティが作成されます。
package main
import (
"context"
"log"
"entdemo/ent"
_ "github.com/lib/pq"
)
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=postgres dbname=entdemo password=password sslmode=disable")
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)
}
CreateUser(context.Background(), client)
}
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
u, err := client.User.
Create().
SetAge(30).
SetName("a8m").
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating user: %w", err)
}
log.Println("user was created: ", u)
return u, nil
}
go run start.go
をターミナルで実行して、以下のメッセージが表示されればOKです。
2024/01/02 16:35:10 user was created: User(id=1, age=30, name=a8m)
初回なので、念のためにレコードも確認してみましょう。
ターミナルで、以下のようにusers
テーブルのレコードを検索してみると、しっかりとレコードが登録されていることが確認できます。
entdemo=# select * from users;
id | age | name
----+-----+------
1 | 30 | a8m
(1 row)
4-5. エンティティの問い合わせ
ent
は、各エンティティのスキーマに対して、SQLの述語、デフォルト値、バリデーション、ストレージ要素に対する追加情報を自動生成してくれます。
それらを利用して、以下のような処理を記述することで、エンティティの問い合わせが可能です。
package main
import (
"context"
"fmt"
"log"
"entdemo/ent"
"entdemo/ent/user"
_ "github.com/lib/pq"
)
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=postgres dbname=entdemo password=password sslmode=disable")
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)
}
QueryUser(context.Background(), client)
}
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
u, err := client.User.
Query().
Where(user.Name("a8m")).
// `Only` fails if no user found,
// or more than 1 user returned.
Only(ctx)
if err != nil {
return nil, fmt.Errorf("failed querying user: %w", err)
}
log.Println("user returned: ", u)
return u, nil
}
go run start.go
をターミナルで実行して、以下のメッセージが表示されればOKです。
2024/01/02 16:45:13 user returned: User(id=1, age=30, name=a8m)
Edge
(リレーション)の追加
4-6. ent
では、Edge
と呼ばれる機能を利用してリレーションシップを設定することができます。
ここでは、Car
エンティティとGroup
エンティティを作成して、User
エンティティを参照するEdge
を設定してみましょう。
Car
エンティティとGroup
エンティティの作成
4-6-1. 以下のコマンドを実行して、エンティティを追加します。
go run -mod=mod entgo.io/ent/cmd/ent new Car Group
追加したら、以下のようにFieldを定義して下さい。
// Fields of the Car.
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String(("model")),
field.Time("registered_at"),
}
}
// Fields of the Car.
// Fields of the Group.
func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name").
// 正規表現を使って、nameの値を制限する
Match(regexp.MustCompile("[a-zA-Z_]+$")),
}
}
User
エンティティからCar
エンティティへのEdge
を追加
4-6-2. フィールドの定義ができたら、Edge
を設定してみましょう!
ここでは、ユーザは複数台の車を所有することができ、車にはただ一人のオーナが存在する設定でリレーションを設定します。
要するに、以下のような1対多のリレーションシップです。
Edgeを設定する場合は、以下のようにUser
スキーマのEdge()
関数に記述をします。
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}
次にgo generate ./ent
を実行して、関連するファイルを生成して下さい。
あとは、go run start.go
を実行してマイグレーションを行うだけです。
Edge
の確認
4-6-2-1. テーブル定義における念のために以下のコマンドを実行して、作成されたテーブルとテーブル定義も確認しておきましょう。
まずは、テーブルからです。
ちゃんと、cars
とgroups
の名前でテーブルが作成されています。
entdemo=# \dt;
List of relations
Schema | Name | Type | Owner
--------+--------+-------+----------
public | cars | table | postgres
public | groups | table | postgres
public | users | table | postgres
(3 rows)
次に、テーブル定義を確認しましょう。
users
テーブルでは、末尾にReferenced by: ~
が追加されており、cars
テーブルから外部キー制約が設定されていることがわかります。
entdemo=# \d users;
Table "public.users"
Column | Type | Collation | Nullable | Default
--------+-------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
age | bigint | | not null |
name | character varying | | not null | 'unknown'::character varying
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
Referenced by:
TABLE "cars" CONSTRAINT "cars_users_cars" FOREIGN KEY (user_cars) REFERENCES users(id) ON DELETE SET NULL
cars
テーブルの定義は以下のように設定されており、users
への外部キー制約を確認できます。
entdemo=# \d cars;
Table "public.cars"
Column | Type | Collation | Nullable | Default
---------------+--------------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
model | character varying | | not null |
registered_at | timestamp with time zone | | not null |
user_cars | bigint | | |
Indexes:
"cars_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"cars_users_cars" FOREIGN KEY (user_cars) REFERENCES users(id) ON DELETE SET NULL
User
エンティティへCar
エンティティを登録する
4-6-3. これで、やっとUser
エンティティに対してCar
エンティティを登録することができます。
以下のようにstart.go
を記述して、go run start.go
を実行して下さい。
package main
import (
"context"
"fmt"
"log"
"time"
"entdemo/ent"
_ "github.com/lib/pq"
)
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=postgres dbname=entdemo password=password sslmode=disable")
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)
}
CreateCars(context.Background(), client)
}
func CreateCars(ctx context.Context, client *ent.Client) (*ent.User, error) {
// Create a new car with model "Tesla".
tesla, err := client.Car.
Create().
SetModel("Tesla").
SetRegisteredAt(time.Now()).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating car: %w", err)
}
log.Println("car was created: ", tesla)
// Create a new car with model "Ford".
ford, err := client.Car.
Create().
SetModel("Ford").
SetRegisteredAt(time.Now()).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating car: %w", err)
}
log.Println("car was created: ", ford)
// Create a new user, and add it the 2 cars.
a8m, err := client.User.
Create().
SetAge(30).
SetName("a8m").
AddCars(tesla, ford).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating user: %w", err)
}
log.Println("user was created: ", a8m)
return a8m, nil
}
正常に登録ができると、以下のように出力されます。
2024/01/02 18:08:17 car was created: Car(id=1, model=Tesla, registered_at=Tue Jan 2 18:08:16 2024)
2024/01/02 18:08:17 car was created: Car(id=2, model=Ford, registered_at=Tue Jan 2 18:08:17 2024)
2024/01/02 18:08:17 user was created: User(id=2, age=30, name=a8m)
また、users
とcars
のレコードは以下のように登録されています。
entdemo=# select * from users;
id | age | name
----+-----+------
1 | 30 | a8m
2 | 30 | a8m
(2 rows)
entdemo=# select * from cars;
id | model | registered_at | user_cars
----+-------+-------------------------------+-----------
1 | Tesla | 2024-01-02 09:08:16.99925+00 | 2
2 | Ford | 2024-01-02 09:08:17.010137+00 | 2
(2 rows)
User
エンティティからCar
エンティティを参照する
4-6-4. もちろん、User
エンティティから、それに紐づいているCar
エンティティを参照することが可能です。
参照方法は非常に簡単で、以下のようにUser
エンティティから述語に関連するメソッドを実行するだけです。
package main
import (
"context"
"fmt"
"log"
"entdemo/ent"
"entdemo/ent/car"
"entdemo/ent/user"
_ "github.com/lib/pq"
)
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=postgres dbname=entdemo password=password sslmode=disable")
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)
}
a8m, err := client.User.Query().Where(user.NameEQ("a8m")).All(context.Background())
if err != nil {
log.Fatalf("failed querying user: %v", err)
}
// 初回に追加したユーザ
QueryCars(context.Background(), a8m[0])
// CreateCarsで追加されたユーザ
QueryCars(context.Background(), a8m[1])
}
func QueryCars(ctx context.Context, a8m *ent.User) error {
cars, err := a8m.QueryCars().All(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
log.Println("returned cars:", cars)
// What about filtering specific cars.
ford, err := a8m.QueryCars().
Where(car.Model("Ford")).
Only(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
log.Println(ford)
return nil
}
正常に検索が完了すると、以下のように出力されます。
2024/01/02 18:40:22 returned cars: []
2024/01/02 18:40:22 returned cars: [Car(id=1, model=Tesla, registered_at=Tue Jan 2 09:08:16 2024) Car(id=2, model=Ford, registered_at=Tue Jan 2 09:08:17 2024)]
2024/01/02 18:40:22 Car(id=2, model=Ford, registered_at=Tue Jan 2 09:08:17 2024)
Car
エンティティからUser
エンティティを参照する
4-7. Car
エンティティの所有者を参照したい場合は多々あるかと思いますが、今のままのスキーマ定義ではそれを実現することは叶いません。
それを行うにはedge.From
関数を用いてinverse edgeと呼ばれるEdge
を設定する必要があります(下図参照)
上図で新たに作成されたownerのエッジは色が薄くなっているのは、「データベースに別のエッジを作成しない」ことを強調するためです。これは実際のエッジ(リレーション)への後方参照に過ぎません。
では、以下のようにCar
スキーマにowner
という名前でinverse edgeを設定し、go generate ./ent
を実行しましょう。
// Edges of the Car.
func (Car) Edges() []ent.Edge {
return []ent.Edge{
// Create an inverse-edge called "owner" of type `User`
// and reference it to the "cars" edge (in User schema)
// explicitly using the `Ref` method.
edge.From("owner", User.Type).
Ref("cars").
// setting the edge to unique, ensure
// that a car can have only one owner.
Unique(),
}
}
あとは、以下のようにCar
エンティティからUser
エンティティを参照する関数を実装するだけです。
package main
import (
"context"
"fmt"
"log"
"entdemo/ent"
"entdemo/ent/user"
_ "github.com/lib/pq"
)
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=postgres dbname=entdemo password=password sslmode=disable")
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)
}
a8m, err := client.User.Query().Where(user.NameEQ("a8m")).All(context.Background())
if err != nil {
log.Fatalf("failed querying user: %v", err)
}
QueryCarUsers(context.Background(), a8m[1])
}
func QueryCarUsers(ctx context.Context, a8m *ent.User) error {
cars, err := a8m.QueryCars().All(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
// Query the inverse edge.
for _, c := range cars {
owner, err := c.QueryOwner().Only(ctx)
if err != nil {
return fmt.Errorf("failed querying car %q owner: %w", c.Model, err)
}
log.Printf("car %q owner: %q\n", c.Model, owner.Name)
}
return nil
}
正常に動作すると、以下のように出力されます。
2024/01/02 19:34:25 car "Tesla" owner: "a8m"
2024/01/02 19:34:25 car "Ford" owner: "a8m"
念のために、テーブル定義も確認しておきましょう。
以下のように、テーブル定義を確認しても新たに外部キーなどが設定されていないことが確認できます。
entdemo=# \d users;
Table "public.users"
Column | Type | Collation | Nullable | Default
--------+-------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
age | bigint | | not null |
name | character varying | | not null | 'unknown'::character varying
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
Referenced by:
TABLE "cars" CONSTRAINT "cars_users_cars" FOREIGN KEY (user_cars) REFERENCES users(id) ON DELETE SET NULL
entdemo=# \d cars;
Table "public.cars"
Column | Type | Collation | Nullable | Default
---------------+--------------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
model | character varying | | not null |
registered_at | timestamp with time zone | | not null |
user_cars | bigint | | |
Indexes:
"cars_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"cars_users_cars" FOREIGN KEY (user_cars) REFERENCES users(id) ON DELETE SET NULL
Edge
を追加する
4-8. 多対多のここでは、複数のユーザが、複数のグループに属している関係を表現するために、多対多のEdge
を追加していきます。
上図のような関係を構築するには、下記のコードのようにGroup
スキーマからは通常のEdge
を設定し、User
スキーマからはinverse edgeを設定します。そうすることで多対多を表現可能です。
// Edges of the Group.
func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
// Create an inverse-edge called "groups" of type `Group`
// and reference it to the "users" edge (in Group schema)
// explicitly using the `Ref` method.
edge.From("groups", Group.Type).
Ref("users"),
}
}
あとは、いつも通りにスキーマを変更したらgo generate ./ent
を実行して、対応するコードを自動生成して下さい。
4-8-1. 多対多の中間テーブルを確認
先ほどUser
とGroup
に多対多のリレーションを設定しましたが、当然中間テーブルが構築されているはずです。
念のために、以下のようにターミナル上で確認してみましょう。
entdemo=# \dt;
List of relations
Schema | Name | Type | Owner
--------+-------------+-------+----------
public | cars | table | postgres
public | group_users | table | postgres
public | groups | table | postgres
public | users | table | postgres
(4 rows)
entdemo=# \d group_users;
Table "public.group_users"
Column | Type | Collation | Nullable | Default
----------+--------+-----------+----------+---------
group_id | bigint | | not null |
user_id | bigint | | not null |
Indexes:
"group_users_pkey" PRIMARY KEY, btree (group_id, user_id)
Foreign-key constraints:
"group_users_group_id" FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
"group_users_user_id" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
中間テーブルgroup_users
が生成されており、テーブル定義もusers
とgroups
への外部キーとなるgroup_id
とuser_id
が定義されていることが確認できました。
また、groups
とusers
のテーブル定義も以下のように、group_users
テーブルへの参照が加えられています。
entdemo=# \d groups;
Table "public.groups"
Column | Type | Collation | Nullable | Default
--------+-------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
name | character varying | | not null |
Indexes:
"groups_pkey" PRIMARY KEY, btree (id)
Referenced by:
TABLE "group_users" CONSTRAINT "group_users_group_id" FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
entdemo=# \d users;
Table "public.users"
Column | Type | Collation | Nullable | Default
--------+-------------------+-----------+----------+----------------------------------
id | bigint | | not null | generated by default as identity
age | bigint | | not null |
name | character varying | | not null | 'unknown'::character varying
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
Referenced by:
TABLE "cars" CONSTRAINT "cars_users_cars" FOREIGN KEY (user_cars) REFERENCES users(id) ON DELETE SET NULL
TABLE "group_users" CONSTRAINT "group_users_user_id" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
4-9. グラフ構造の探索
最後に、下図のような関係性を持ったグラフを作成し、それらに対してクエリを実行してみましょう。
4-9-1. データの作成
まずは、以下のコードを実行して、上図と同じ関係性を持ったグラフを作成します。
package main
import (
"context"
"log"
"time"
"entdemo/ent"
_ "github.com/lib/pq"
)
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=postgres dbname=entdemo password=password sslmode=disable")
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)
}
if err := CreateGraph(context.Background(), client); err != nil {
log.Fatalf("failed creating graph: %v", err)
}
}
func CreateGraph(ctx context.Context, client *ent.Client) error {
// First, create the users.
a8m, err := client.User.
Create().
SetAge(30).
SetName("Ariel").
Save(ctx)
if err != nil {
return err
}
neta, err := client.User.
Create().
SetAge(28).
SetName("Neta").
Save(ctx)
if err != nil {
return err
}
// Then, create the cars, and attach them to the users created above.
err = client.Car.
Create().
SetModel("Tesla").
SetRegisteredAt(time.Now()).
// Attach this car to Ariel.
SetOwner(a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Car.
Create().
SetModel("Mazda").
SetRegisteredAt(time.Now()).
// Attach this car to Ariel.
SetOwner(a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Car.
Create().
SetModel("Ford").
SetRegisteredAt(time.Now()).
// Attach this car to Neta.
SetOwner(neta).
Exec(ctx)
if err != nil {
return err
}
// Create the groups, and add their users in the creation.
err = client.Group.
Create().
SetName("GitLab").
AddUsers(neta, a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Group.
Create().
SetName("GitHub").
AddUsers(a8m).
Exec(ctx)
if err != nil {
return err
}
log.Println("The graph was created successfully")
return nil
}
4-9-1. Githubに属するユーザの所有車を検索する
以下のように、WhereメソッドからGroupによる絞り込みが行えます。
package main
import (
"context"
"fmt"
"log"
"entdemo/ent"
"entdemo/ent/group"
_ "github.com/lib/pq"
)
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=postgres dbname=entdemo password=password sslmode=disable")
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)
}
QueryGithub(context.Background(), client)
}
func QueryGithub(ctx context.Context, client *ent.Client) error {
cars, err := client.Group.
Query().
Where(group.Name("GitHub")). // (Group(Name=GitHub),)
QueryUsers(). // (User(Name=Ariel, Age=30),)
QueryCars(). // (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
All(ctx)
if err != nil {
return fmt.Errorf("failed getting cars: %w", err)
}
log.Println("cars returned:", cars)
// Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
return nil
}
上記のコードが正常に動作すると、以下のように出力されます。
2024/01/04 19:31:42 cars returned: [Car(id=6, model=Tesla, registered_at=Thu Jan 4 10:27:21 2024) Car(id=7, model=Mazda, registered_at=Thu Jan 4 10:27:21 2024)]
Ariel
にした場合
4-9-2. 検索の始点を4-9-1項のコードを少し修正して、以下のようにしてCarを検索することも可能です。
package main
import (
"context"
"fmt"
"log"
"entdemo/ent"
"entdemo/ent/car"
"entdemo/ent/user"
_ "github.com/lib/pq"
)
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=postgres dbname=entdemo password=password sslmode=disable")
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)
}
QueryArielCars(context.Background(), client)
}
func QueryArielCars(ctx context.Context, client *ent.Client) error {
// Get "Ariel" from previous steps.
a8m := client.User.
Query().
Where(
user.HasCars(),
user.Name("Ariel"),
).
OnlyX(ctx)
cars, err := a8m. // Get the groups, that a8m is connected to:
QueryGroups(). // (Group(Name=GitHub), Group(Name=GitLab),)
QueryUsers(). // (User(Name=Ariel, Age=30), User(Name=Neta, Age=28),)
QueryCars(). // (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
Where( //
car.Not( // Get Neta and Ariel cars, but filter out
car.Model("Mazda"), // those who named "Mazda"
), //
). //
All(ctx)
if err != nil {
return fmt.Errorf("failed getting cars: %w", err)
}
log.Println("cars returned:", cars)
// Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Ford, RegisteredAt=<Time>),)
return nil
}
上記のコードの実行結果は、以下の通りです。
2024/01/04 19:46:29 cars returned: [Car(id=6, model=Tesla, registered_at=Thu Jan 4 10:27:21 2024) Car(id=8, model=Ford, registered_at=Thu Jan 4 10:27:21 2024)]
4-9-3. ユーザが1人以上所属しているグループの検索
最後に、ユーザが1人以上所属しているグループを検索してみましょう。
以下のコードを実行することで、簡単に検索することが可能です。
package main
import (
"context"
"fmt"
"log"
"entdemo/ent"
"entdemo/ent/group"
_ "github.com/lib/pq"
)
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=postgres dbname=entdemo password=password sslmode=disable")
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)
}
QueryGroupWithUsers(context.Background(), client)
}
func QueryGroupWithUsers(ctx context.Context, client *ent.Client) error {
groups, err := client.Group.
Query().
Where(group.HasUsers()).
All(ctx)
if err != nil {
return fmt.Errorf("failed getting groups: %w", err)
}
log.Println("groups returned:", groups)
// Output: (Group(Name=GitHub), Group(Name=GitLab),)
return nil
}
上記のコードの実行結果は、以下の通りです。
2024/01/04 19:57:54 groups returned: [Group(id=3, name=GitLab) Group(id=4, name=GitHub)]
まとめ
Quick Introductionに気持ちばかりの加筆をした記事になってしまいましたが、私の周囲ではent.の知名度があまり高くないので、とりあえず日本語記事が増えればいいやぐらいの気持ちで書きました。
この記事では、Atlasを用いたVersioned MigrationsやVisualize the Schemaを省いてしまいましたが、別記事でこれらの使い方も書こうと思います。(個人的には、Visualize the Schemaはめちゃくちゃ便利だと思ってます)
Discussion