几个月前,Ent 项目宣布了Schema Import Initiative,其目标是帮助支持许多从外部资源生成 Ent 模式的用例。今天,我很高兴分享一个我一直在工作的项目:entimport——一个 importent(双关)命令行工具,旨在从现有 SQL 数据库创建 Ent 模式。此功能已被社区多次请求,因此我希望许多人会觉得它很有用。它可以帮助简化从其他语言或 ORM 转移到 Ent 的现有设置,并可用于在不同平台(如自动同步)之间访问相同数据的用例。
第一个版本同时支持 MySQL 和 PostgreSQL 数据库,并在下文中描述了一些限制。对其他关系型数据库(如 SQLite)的支持正在进行中。
Getting Started
为了让您了解 entimport 的工作方式,我想分享一个使用 MySQL 数据库的端到端示例。在大致上,我们要做的是:
- 创建数据库和模式——我们将展示如何让
entimport为现有数据库生成 Ent 模式。我们将先创建数据库,然后定义一些可以导入 Ent 的表。 - 初始化 Ent 项目——我们将使用 Ent CLI 创建所需的目录结构和 Ent 模式代码生成脚本。
- 安装
entimport - 运行
entimport并导入我们的演示数据库——随后我们将把刚创建的数据库模式导入 Ent 项目。 - 说明如何在 Ent 中使用生成的模式。
Create a Database
我们将从创建数据库开始。我个人更喜欢使用 Docker 容器来完成这一步。我们将使用 docker-compose,它会自动向 MySQL 容器传递所有必要参数。
在名为 entimport-example 的新目录中开始项目。创建一个名为 docker-compose.yaml 的文件,并粘贴以下内容:
version: "3.7"
services:
mysql8:
platform: linux/amd64
image: mysql
environment:
MYSQL_DATABASE: entimport
MYSQL_ROOT_PASSWORD: pass
healthcheck:
test: mysqladmin ping -ppass
ports:
- "3306:3306"
此文件包含 MySQL Docker 容器的服务配置。使用以下命令运行:
docker-compose up -d
接下来,我们将创建一个简单的模式。本示例使用两个实体之间的关联:
- User
- Car
使用 MySQL Shell 连接到数据库,您可以使用以下命令完成此操作:
确保从项目根目录运行。
docker-compose exec mysql8 mysql --database=entimport -ppass
create table users
(
id bigint auto_increment primary key,
age bigint not null,
name varchar(255) not null,
last_name varchar(255) null comment 'surname'
);
create table cars
(
id bigint auto_increment primary key,
model varchar(255) not null,
color varchar(255) not null,
engine_size mediumint not null,
user_id bigint null,
constraint cars_owners foreign key (user_id) references users (id) on delete set null
);
验证已创建上述表,在 MySQL Shell 中运行:
show tables;
+---------------------+
| Tables_in_entimport |
+---------------------+
| cars |
| users |
+---------------------+
我们应该能看到两个表:users & cars。
Initialize Ent Project
现在我们已经创建了数据库和演示的基本模式,需要创建一个基于 Ent 的 Go 项目。本阶段我将解释如何操作。由于最终我们想使用导入的模式,必须创建 Ent 目录结构。
在名为 entimport-example 的目录下初始化一个新的 Go 项目:
go mod init entimport-example
运行 Ent Init:
go run -mod=mod entgo.io/ent/cmd/ent new
项目结构应如下:
├── docker-compose.yaml
├── ent
│ ├── generate.go
│ └── schema
└── go.mod
Install entimport
好的,开始真正的工作!我们终于准备好安装 entimport 并查看其效果。
让我们先运行 entimport:
go run -mod=mod ariga.io/entimport/cmd/entimport -h
entimport 将被下载,命令将打印:
Usage of entimport:
-dialect string
database dialect (default "mysql")
-dsn string
data source name (connection information)
-schema-path string
output path for ent schema (default "./ent/schema")
-tables value
comma-separated list of tables to inspect (all if empty)
Run entimport
我们现在已准备好将 MySQL 模式导入 Ent!
我们将使用以下命令执行:
该命令将导入我们模式中的所有表,也可通过
-tables标志限制特定表。
go run ariga.io/entimport/cmd/entimport -dialect mysql -dsn "root:pass@tcp(localhost:3306)/entimport"
与许多 Unix 工具一样,entimport 在成功运行后不会打印任何内容。为验证它已正常完成,我们将检查文件系统,尤其是 ent/schema 目录:
├── docker-compose.yaml
├── ent
│ ├── generate.go
│ └── schema
│ ├── car.go
│ └── user.go
├── go.mod
└── go.sum
看看这到底生成了什么——记住我们有两个模式:users 模式和一个一对多关系的 cars 模式。让我们看一下 entimport 的效果。
type User struct {
ent.Schema
}
func (User) Fields() []ent.Field {
return []ent.Field{field.Int("id"), field.Int("age"), field.String("name"), field.String("last_name").Optional().Comment("surname")}
}
func (User) Edges() []ent.Edge {
return []ent.Edge{edge.To("cars", Car.Type)}
}
func (User) Annotations() []schema.Annotation {
return nil
}
type Car struct {
ent.Schema
}
func (Car) Fields() []ent.Field {
return []ent.Field{field.Int("id"), field.String("model"), field.String("color"), field.Int32("engine_size"), field.Int("user_id").Optional()}
}
func (Car) Edges() []ent.Edge {
return []ent.Edge{edge.From("user", User.Type).Ref("cars").Unique().Field("user_id")}
}
func (Car) Annotations() []schema.Annotation {
return nil
}
entimport成功创建了实体及其关系!
到此为止,一切看起来不错。接下来让我们实际尝试它们。首先需要生成 Ent 模式。我们之所以这样做,是因为 Ent 是一个 schema first ORM,能够为不同数据库生成 Go 代码来进行交互。
运行 Ent 代码生成:
go generate ./ent
查看我们的 ent 目录:
...
├── ent
│ ├── car
│ │ ├── car.go
│ │ └── where.go
...
│ ├── schema
│ │ ├── car.go
│ │ └── user.go
...
│ ├── user
│ │ ├── user.go
│ │ └── where.go
...
Ent Example
快速运行一个示例来验证我们的模式是否正常工作:
在项目根目录中创建名为 example.go 的文件,内容如下:
本例的示例可在此处查看:part1/example.go
package main
import (
"context"
"fmt"
"log"
"entimport-example/ent"
"entgo.io/ent/dialect"
_ "github.com/go-sql-driver/mysql"
)
func main() {
client, err := ent.Open(dialect.MySQL, "root:pass@tcp(localhost:3306)/entimport?parseTime=True")
if err != nil {
log.Fatalf("failed opening connection to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
example(ctx, client)
}
尝试添加一个用户,在文件末尾写入以下代码:
func example(ctx context.Context, client *ent.Client) {
// Create a User.
zeev := client.User.
Create().
SetAge(33).
SetName("Zeev").
SetLastName("Manilovich").
SaveX(ctx)
fmt.Println("用户已创建:", zeev)
}
然后运行:
go run example.go
输出应为:
用户已创建: User(id=1, age=33, name=Zeev, last_name=Manilovich)
通过查询数据库确认用户已成功添加:
SELECT *
FROM users
WHERE name = 'Zeev';
+--+---+----+----------+
|id|age|name|last_name |
+--+---+----+----------+
|1 |33 |Zeev|Manilovich|
+--+---+----+----------+
太好了!现在让我们使用 Ent 进一步添加关系,随后在 example() 函数末尾加入以下代码:
确保在
import()声明中添加"entimport-example/ent/user"。
// Create Car.
vw := client.Car.
Create().
SetModel("volkswagen").
SetColor("blue").
SetEngineSize(1400).
SaveX(ctx)
fmt.Println("第一次汽车已创建:", vw)
// Update the user - add the car relation.
client.User.Update().Where(user.ID(zeev.ID)).AddCars(vw).SaveX(ctx)
// Query all cars that belong to the user.
cars := zeev.QueryCars().AllX(ctx)
fmt.Println("用户的车辆:", cars)
// Create a second Car.
delorean := client.Car.
Create().
SetModel("delorean").
SetColor("silver").
SetEngineSize(9999).
SaveX(ctx)
fmt.Println("第二辆车已创建:", delorean)
// Update the user - add another car relation.
client.User.Update().Where(user.ID(zeev.ID)).AddCars(delorean).SaveX(ctx)
// Traverse the sub-graph.
cars = delorean.
QueryUser().
QueryCars().
AllX(ctx)
fmt.Println("用户的车辆:", cars)
本例的示例可在此处查看:part2/example.go
现在执行 go run example.go。运行上述代码后,数据库中应包含一个用户及其关联的两辆车,形成一对多关系。
SELECT *
FROM users;
+--+---+----+----------+
|id|age|name|last_name |
+--+---+----+----------+
|1 |33 |Zeev|Manilovich|
+--+---+----+----------+
SELECT *
FROM cars;
+--+----------+------+-----------+-------+
|id|model |color |engine_size|user_id|
+--+----------+------+-----------+-------+
|1 |volkswagen|blue |1400 |1 |
|2 |delorean |silver|9999 |1 |
+--+----------+------+-----------+-------+
Syncing DB changes
既然我们想保持数据库同步,我们希望 entimport 能在数据库变更后修改模式。让我们看看它的工作方式。
执行以下 SQL 代码,在 users 表中添加一个 phone 列并创建唯一索引:
alter table users
add phone varchar(255) null;
create unique index users_phone_uindex
on users (phone);
表结构应如下所示:
describe users;
+-----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+----------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| age | bigint | NO | | NULL | |
| name | varchar(255) | NO | | NULL | |
| last_name | varchar(255) | YES | | NULL | |
| phone | varchar(255) | YES | UNI | NULL | |
+-----------+--------------+------+-----+---------+----------------+
再次运行 entimport 以获取数据库的最新模式:
go run -mod=mod ariga.io/entimport/cmd/entimport -dialect mysql -dsn "root:pass@tcp(localhost:3306)/entimport"
我们可以看到 user.go 文件已被修改:
func (User) Fields() []ent.Field {
return []ent.Field{field.Int("id"), ..., field.String("phone").Optional().Unique()}
}
现在我们可以再次运行 go generate ./ent 并使用新模式向 User 实体添加 phone 字段。
Future Plans
如上所述,初始版本支持 MySQL 和 PostgreSQL 数据库,并且支持所有类型的 SQL 关系。我计划进一步升级该工具,并增加诸如缺失的 PostgreSQL 字段、默认值等功能。
Wrapping Up
在本文中,我介绍了 entimport,这是一款多次被 Ent 社区期待并请求的工具。我演示了如何将其与 Ent 一起使用。此工具是 Ent 模式导入工具的又一次补充,旨在使 ent 的集成更为便捷。若有讨论和支持需求,请 创建问题。完整示例可在此处查看:entimport-example。希望你觉得这篇博客文章有用!
- 订阅我们的 新闻简报
- 在 Twitter 上关注我们
- 加入 Gophers Slack 中的 #ent 频道
- 加入我们的 Ent Discord 服务器