ent | Quick Introduction
ent | Quick Introduction
entはGoのためのシンプルで強力なエンティティフレームワークで、大規模なデータモデルを持つアプリケーションの構築と保守を容易にし、以下の原則に忠実です:
- データベーススキーマをグラフ構造として簡単にモデル化できる。
- スキーマをプログラム的なGoコードとして定義する。
- コード生成に基づく静的型付け
- データベースクエリやグラフトラバーサルを簡単に書くことができる。
- Goテンプレートを使って拡張やカスタマイズが簡単にできる。
環境構築
go.modを作成します。
go mod init entdemo
スキーマ作成
プロジェクトのルートディレクトリに移動して、下記のコマンドを実行します
go run -mod=mod entgo.io/ent/cmd/ent new User
上記のコマンドで、entdemo/ent/schema/
ディレクトリの下にUser
のスキーマが生成されます
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
}
User
スキーマに2つのフィールドを追加します
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"),
}
}
プロジェクトのルートディレクトリから、以下のようにgo generate
を実行します
go generate ./ent
以下のようなファイルが生成されます。
ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... truncated
├── schema
│ └── user.go
├── tx.go
**├── user
│ ├── user.go
│ └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go**
エンティティ作成
まずは、スキーママイグレーションを実行し、クライアントを作成します
下記の例はPostgreSQLです。
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)
}
}
実行すると、スキーマが作成されます。
db=# \d
List of relations
Schema | Name | Type | Owner
--------+--------------+----------+-------
public | users | table | user
public | users_id_seq | sequence | user
(2 rows)
users
テーブルにユーザーを追加します。
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
}
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=user dbname=db password=pass")
if err != nil {
log.Fatalf("failed opening connection to postgres: %v", err)
}
defer client.Close()
ctx := context.Background()
CreateUser(ctx, client)
}
実行すると、ユーザーが追加されます。
db=# select * from users;
id | age | name
----+-----+------
1 | 30 | a8m
(1 row)
クエリを投げる
entは、各エンティティスキーマに対して、その述語、デフォルト値、バリデータ、ストレージ要素に関する追加情報(カラム名、主キーなど)を含むパッケージを生成します。
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
}
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=user dbname=db password=pass")
if err != nil {
log.Fatalf("failed opening connection to postgres: %v", err)
}
defer client.Close()
ctx := context.Background()
QueryUser(ctx, client)
}
↓実行結果
$ go run start.go
2023/06/02 02:24:31 user returned: User(id=1, age=30, name=a8m)
リレーションを追加する
下記コマンドを実行し、いくつかのフィールドを持つCar
とGroup
という名前の2つの追加エンティティを作成します。
go run -mod=mod entgo.io/ent/cmd/ent new Car Group
そして、残りのフィールドを手動で追加します
// Fields of the Car.
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String("model"),
field.Time("registered_at"),
}
}
// Fields of the Group.
func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name").
// Regexp validation for group name.
Match(regexp.MustCompile("[a-zA-Z_]+$")),
}
}
ユーザーから車へのエッジは、ユーザーは1台以上の車を持つことができるが、車のオーナーは1人だけであることを定義する(一対多の関係)。
Userスキーマに cars
エッジを追加し、go generate ./ent
を実行しましょう:
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}
2台の車を作成し、ユーザーに追加します。
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
}
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=user dbname=db password=pass")
if err != nil {
log.Fatalf("failed opening connection to postgres: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run the auto migration tool.
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
CreateCars(ctx, client)
}
cars
テーブルとusers
テーブルにデータが追加されます。
db=# select * from cars;
id | model | registered_at | user_cars
----+-------+-------------------------------+-----------
1 | Tesla | 2023-06-02 04:35:56.283204+00 | 2
2 | Ford | 2023-06-02 04:35:56.315002+00 | 2
(2 rows)
db=# select * from users;
id | age | name
----+-----+------
1 | 30 | a8m
2 | 30 | a8m
(2 rows)
車のエッジをクエリします。
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
u, err := client.User.
Query().
Where(user.ID(2)).
// `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
}
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
}
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=user dbname=db password=pass")
if err != nil {
log.Fatalf("failed opening connection to postgres: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run the auto migration tool.
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
u, err := QueryUser(ctx, client)
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
QueryCars(ctx, u)
}
↓実行結果
$ go run start.go
2023/06/02 05:09:15 user returned: User(id=2, age=30, name=a8m)
2023/06/02 05:09:15 returned cars: [Car(id=1, model=Tesla, registered_at=Fri Jun 2 04:35:56 2023) Car(id=2, model=Ford, registered_at=Fri Jun 2 04:35:56 2023)]
2023/06/02 05:09:15 Car(id=2, model=Ford, registered_at=Fri Jun 2 04:35:56 2023)
逆向きのエッジを追加する
例えば、Car
オブジェクトがあり、その所有者、つまりこの車が属するユーザーを取得したいとします。この場合、edge.From
関数で定義される「逆縁」という別のタイプの縁があります。
上の図で新しく作られたエッジは半透明になっていますが、これはデータベースに別のエッジを作らないということを強調するためです。これは、実際のエッジ(関係)への逆参照に過ぎません。
Car
スキーマにowner
という名前の逆エッジを追加し、User
スキーマのcars
エッジに参照させ、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(),
}
}
上記のuser/carsの例に引き続き、逆エッジを問い合わせることにします。
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
u, err := client.User.
Query().
Where(user.ID(2)).
// `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
}
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
}
func main() {
client, err := ent.Open("postgres", "host=localhost port=5432 user=user dbname=db password=pass")
if err != nil {
log.Fatalf("failed opening connection to postgres: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run the auto migration tool.
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
u, err := QueryUser(ctx, client)
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
QueryCarUsers(ctx, u)
}
$ go run start.go
2023/06/02 05:56:16 user returned: User(id=2, age=30, name=a8m)
2023/06/02 05:56:16 car "Tesla" owner: "a8m"
2023/06/02 05:56:16 car "Ford" owner: "a8m"
生成されたSQLスキーマを見る
Entがデータベース用に生成したSQLスキーマを表示するには、Atlasをインストールし、次のコマンドを実行します
Atlasは、ソフトウェアエンジニア、DBA、DevOps実務者がデータベーススキーマを管理するために設計されたオープンソースのツールです。Atlasのユーザーは、Atlas DDL(データ定義言語)またはSQLを使用して目的のデータベーススキーマを記述し、コマンドラインツールを使用してシステムへの移行を計画および適用できます。
Dockerで実行します。
docker pull arigaio/atlas
docker run --rm arigaio/atlas --help
コンテナがホストネットワークまたはローカルディレクトリにアクセスする必要がある場合、--net=hostフラグを使用し、目的のディレクトリをマウントします
docker run --rm --net=host \
-v "$(pwd)/migrations:/migrations" \
arigaio/atlas migrate apply \
--url "postgres://user:pass@:5432/db"
スキーマを調査する
atlas schema inspect \
-u "ent://ent/schema" \
--dev-url "sqlite://file?mode=memory&_fk=1" \
--format '{{ sql . " " }}'
↓実行結果
-- Create "cars" table
CREATE TABLE `cars` (
`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,
`model` text NOT NULL,
`registered_at` datetime NOT NULL,
`user_cars` integer NULL,
CONSTRAINT `cars_users_cars` FOREIGN KEY (`user_cars`) REFERENCES `users` (`id`) ON DELETE SET NULL
);
-- Create "groups" table
CREATE TABLE `groups` (
`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,
`name` text NOT NULL
);
-- Create "users" table
CREATE TABLE `users` (
`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,
`age` integer NOT NULL,
`name` text NOT NULL DEFAULT 'unknown'
);
Discussion
diagram は、mermaid を使うと良いですよ。