Go ORM框架ent快速上手
https://entgo.io/docs/getting-started
ent is a simple, yet powerful entity framework for Go, that makes it easy to build and maintain applications with large data-models and sticks with the following principles:
- Easily model database schema as a graph structure.
- Define schema as a programmatic Go code.
- Static typing based on code generation.
- Database queries and graph traversals are easy to write.
- Simple to extend and customize using Go templates.
一、项目初构
项目目录初始化:
go mod init <project> go get -d entgo.io/ent/cmd/ent
schema创建:
// 生成ent/schema/user.go文件 go run -mod=mod entgo.io/ent/cmd/ent init User go generate ./ent
初始化之后的User对象没有字段,我们为User加几个字段:
// Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name").NotEmpty(), field.Int8("age").Positive(), field.String("address"), } }
接下来生成各种相关源码文件,这些.go文件就是我们可以便捷操纵User对象的基础,他们提供了大量的方法可以让我们像写SQL那样通过调用相应的方法来操作数据库数据:
go generate ./ent // 我们可以进行手动编辑的的其实就只有ent/schema/下的文件,例如这里的user.go,其他的都是自动生成的不要修改:Code generated by ent, DO NOT EDIT. // 修改schema下go文件后务必重复执行一遍go generate ./ent,相应的文件会也会自适应 // 修改schema目录下.go文件后重新执行table create操作时,ent会根据现有表结构做适配(alter table...),不会删除原表,如果同名表结构和要创建的有冲突则会报错
二、增删改查
接下来就可以使用这个user schema进行表的创建以及增删改查了:
package main import ( "context" _ "github.com/go-sql-driver/mysql" "github.com/realcp1018/tinylog" "os" "test/ent" "test/ent/user" ) var Logger = tinylog.NewStreamLogger(tinylog.INFO) func main() { dsnStr := "user:password@tcp(host:port)/<database>?parseTime=True" client, err := ent.Open("mysql", dsnStr) if err != nil { Logger.Fatal("failed opening connection to mysql: %v", err) } defer client.Close() CreateOrAlterAllTables(context.Background(), client) CreateUser(context.Background(), client) QueryUser(context.Background(), client) // 这里篇幅所限只展示建表和insert操作,更新和删除操作都差不多不展示了 } // CreateOrAlterAllTables 重复执行时会自动对比新旧表结构并执行create 或 alter语句 func CreateOrAlterAllTables(ctx context.Context, client *ent.Client) { if err := client.Schema.Create(context.Background()); err != nil { Logger.Fatal("failed creating schema resources: %v", err) } Logger.Info("tables created/altered!") } // insert user func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) { u, err := client.User. Create(). SetName("realcp"). SetAge(11). SetAddress("SH"). Save(ctx) if err != nil { Logger.Error("failed creating user: %s", err.Error()) os.Exit(1) } Logger.Info("user was created: %v", u) return u, nil } // select user func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) { u, err := client.User. Query(). Where(user.Name("realcp")). // `Only` fails if no user found, or more than 1 user returned. Only(ctx) if err != nil { Logger.Error("failed query user: %s", err.Error()) os.Exit(1) } Logger.Info("user query returned: %v", u) return u, nil }
数据库中生成了一个users表,并包含我们插入的一条数据。可以看到生成的string字段默认255长度,我们可以在构造field过程中添加额外的限制(validator)来控制其长度,之前只用了NotEmpty和Positive两种validator,更多的validator例如设置默认值、设置字段最大长度、设置timestamp on update、设置添加comment等,使用编辑器自动预测功能就可以展示出来,这里不多列举了。
大多数validator都会被直接加入表的定义,不过也有仅仅在代码层面做限制的validator,就像Positive那样,数据库中添加“非负”的限制需要修改数据类型或者增加check约束,代码中限制会比较简单些。
你还可以自定义额外的validator函数来做代码层面的数据校验,例如字符串类型可使用Validate(fn func(string) error)来添加额外的校验函数。

更多的User操纵方法只要查看自动生成的那4个user_query.go, user_delete.go...文件即可,里边有详细的方法说明,更简单的方式是打几个SQL常用关键字等待补全后F4跳转即可。
三、Edge设置
edge即关系,通过设置edge我们可以便捷的进行对象的关联查询,ent框架通过图结构来组织schema目录下各个对象的关系,以便实现与传统ORM框架相似的、便捷的关联查询,这种schema之间的关系就是用Edge来定义的。
我们之前添加了users表,接下来增加2个对象car和group。
go run -mod=mod entgo.io/ent/cmd/ent init 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个user可以有多个cars,一个car只能有1个user。1个group包含多个users,1个user可以属于多个group。

// Edges of the User To Car, edge name is: cars func (User) Edges() []ent.Edge { return []ent.Edge{ edge.To("cars", Car.Type), } } // 这里的cars是上图中User->Car的edge名,也是Car对应的表名,因此下次执行CreateOrAlterAllTables时会有一个名为cars的表被创建,这符合预期。 // Edges of the Car from User, edge name is: owner 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(), } } // 这里的owner就是上图中User->Car的反向edge名,这种反向edge(edge.From)不会建为表,而是设置为外键,所以不会存在owner表。
// 适配对应的ent代码,以便我们可以创建cars对象并通过edge进行操作 go generate ./ent
创建2个car对象,并创建一个名为chen的user对象,此对象有两辆cars。
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 { Logger.Error("failed create car: %s", err.Error()) os.Exit(1) } Logger.Info("car was created: %v", tesla) // Create a new car with model "Ford". ford, err := client.Car. Create(). SetModel("Ford"). SetRegisteredAt(time.Now()). Save(ctx) if err != nil { Logger.Error("failed create car: %s", err.Error()) os.Exit(1) } Logger.Info("car was created: %v", ford) // Create a new user "chen", and add it the 2 cars. chen, err := client.User. Create(). SetAge(30). SetName("chen"). SetAddress("SH"). AddCars(tesla, ford). Save(ctx) if err != nil { Logger.Error("failed create car: %s", err.Error()) os.Exit(1) } Logger.Info("user was created: %v", chen) return chen, nil }
然后我们就看到cars表被创建,并且包含一个名为user_cars的外键字段,指向users的主键id。这个外键就实现了User与Car的正反关联。
四、Edge查询
func main() { ...... u, _ := QueryUser(context.Background(), client, "chen") c, _ := QueryCar(context.Background(), client, "Tesla") QueryUserByCar(context.Background(), c) QueryCarsByUser(context.Background(), u) } func QueryUser(ctx context.Context, client *ent.Client, userName string) (*ent.User, error) { u, err := client.User. Query(). Where(user.Name(userName)). // `Only` fails if no user found, // or more than 1 user returned. Only(ctx) if err != nil { Logger.Error("failed query user: %s", err.Error()) os.Exit(1) } Logger.Info("user query returned: %v", u) return u, nil } func QueryCar(ctx context.Context, client *ent.Client, carModel string) (*ent.Car, error) { c, err := client.Car.Query().Where(car.Model(carModel)).Only(ctx) if err != nil { Logger.Error("failed query car %s", carModel) os.Exit(1) } Logger.Info("car %s query returned: %v", carModel, c) return c, nil } func QueryCarsByUser(ctx context.Context, u *ent.User) ([]*ent.Car, error) { userCars, err := u.QueryCars().All(ctx) if err != nil { Logger.Error("failed query cars by user %v", u) os.Exit(1) } Logger.Info("user %v has following cars:\n%v", u, userCars) return userCars, nil } func QueryUserByCar(ctx context.Context, c *ent.Car) (*ent.User, error) { carUser, err := c.QueryOwner().Only(ctx) if err != nil { Logger.Error("failed query user by car %v", c) os.Exit(1) } Logger.Info("car %v has a owner %v", c, carUser) return carUser, nil }
2022/08/25 18:39:30.946699 [INFO] [main.go:41] tables created/altered! 2022/08/25 18:39:31.057777 [INFO] [main.go:70] user query returned: User(id=1, name=chen, age=30, address=SH) 2022/08/25 18:39:31.115351 [INFO] [main.go:149] car Tesla query returned: Car(id=1, model=Tesla, registered_at=Thu Aug 25 09:59:18 2022) 2022/08/25 18:39:31.169589 [INFO] [main.go:169] car Car(id=1, model=Tesla, registered_at=Thu Aug 25 09:59:18 2022) has a owner User(id=1, name=chen, age=30, address=SH) 2022/08/25 18:39:31.224800 [INFO] [main.go:159] user User(id=1, name=chen, age=30, address=SH) has following cars: [Car(id=1, model=Tesla, registered_at=Thu Aug 25 09:59:18 2022) Car(id=2, model=Ford, registered_at=Thu Aug 25 09:59:19 2022)]
至此我们就添加完了一个完整的edge,这个edge就是上述graph中的后半部分,即User与Car之间的部分。
五、初步总结和思考
1. 还有什么要做的?
上边的示例中只实现了1个User<=>Car之间的edge,User<=>Group之间的Edge还未实现,这个很简单照猫画虎即可。
2. ent帮我们做了什么?
很显然,ent可以帮助我们快速构件一种大型的关系型图结构,基于此图我们可以便捷的进行SQL查询而不必再使用SQL语句。
3. 有没有裸SQL语句的执行入口?
如果放弃定义edge,或者有某些特殊场景时,我们就需要使用裸SQL连到数据库执行。ent默认并没有裸SQL入口,但是后来作者加了一个feature,可以在generate代码阶段生成一些支持裸SQL的入口。参考:
https://entgo.io/docs/feature-flags#usage
只需要再执行以下指令开启即可:
go run -mod=mod entgo.io/ent/cmd/ent generate --feature privacy,entql ./ent/schema // --feature的可选值参考上述连接,开启裸SQL的支持为 sql/execquery
除了裸sql的feature支持外,还有其他更多的诸如代码层面的行锁、upsert特性等等feature支持,只需要指定相应的--feature项再跑一次ent generate即可开启这些特性。
4. ent数据库连接池?
ent没有自己的连接池,但可以通过ent.NewCLient(ent.Driver(drv))来使用sql.DB的连接池。
package main import ( "time" "<your_project>/ent" "entgo.io/ent/dialect/sql" ) func Open() (*ent.Client, error) { drv, err := sql.Open("mysql", "<mysql-dsn>") if err != nil { return nil, err } // Get the underlying sql.DB object of the driver. db := drv.DB() db.SetMaxIdleConns(10) db.SetMaxOpenConns(100) db.SetConnMaxLifetime(time.Hour) return ent.NewClient(ent.Driver(drv)), nil }
5. ent的migration支持哪些数据库?
migration其含义为:使schema中定义的【Field和Edge】 与 【数据库中的表结构】保持一致的过程。
我们上边使用的client.Schema.Create(context.Background())就是用于实现此过程的,即当我们修改了schema中的表定义后,执行migration会同步更新数据库中的表结构。
目前ent支持的migration数据库有:mysql/mariadb, postgreSQL, CockroachDB, SQLite, TiDB。
所以如何定义edge和使用裸SQL需要权衡,目前我的想法是优先使用edge,极少数特殊场景下可以使用裸SQL获取数据。不过这种东西就和mybatis vs hibernate一样争论不休。
六、最后
本篇笔记只是描述ent的快速上手流程,更多ent特性,例如事务支持、完整的CRUD实例和过滤|聚合|分页等查询、GraphQL支持、自定义模板等功能还有待发掘。