跳到主要内容

从现有 SQL 数据库生成 Ent 模式

· 阅读需 12 分钟

几个月前,Ent 项目宣布了Schema Import Initiative,其目标是帮助支持许多从外部资源生成 Ent 模式的用例。今天,我很高兴分享一个我一直在工作的项目:entimport——一个 importent(双关)命令行工具,旨在从现有 SQL 数据库创建 Ent 模式。此功能已被社区多次请求,因此我希望许多人会觉得它很有用。它可以帮助简化从其他语言或 ORM 转移到 Ent 的现有设置,并可用于在不同平台(如自动同步)之间访问相同数据的用例。

第一个版本同时支持 MySQL 和 PostgreSQL 数据库,并在下文中描述了一些限制。对其他关系型数据库(如 SQLite)的支持正在进行中。

Getting Started

为了让您了解 entimport 的工作方式,我想分享一个使用 MySQL 数据库的端到端示例。在大致上,我们要做的是:

  1. 创建数据库和模式——我们将展示如何让 entimport 为现有数据库生成 Ent 模式。我们将先创建数据库,然后定义一些可以导入 Ent 的表。
  2. 初始化 Ent 项目——我们将使用 Ent CLI 创建所需的目录结构和 Ent 模式代码生成脚本。
  3. 安装 entimport
  4. 运行 entimport 并导入我们的演示数据库——随后我们将把刚创建的数据库模式导入 Ent 项目。
  5. 说明如何在 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 的效果。

entimport-example/ent/schema/user.go
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
}
entimport-example/ent/schema/car.go
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

entimport-example/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)
}

尝试添加一个用户,在文件末尾写入以下代码:

entimport-example/example.go
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"

entimport-example/example.go
// 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 文件已被修改:

entimport-example/ent/schema/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。希望你觉得这篇博客文章有用!

更多 Ent 新闻与更新: