Go ORM框架ent快速上手

https://entgo.io/docs/getting-started

https://github.com/ent/ent

ent是一款facebook开源的go语言ORM框架,类似于gorm等用于实现数据库对象映射和操作的框架。ent的不同之处在于他可以使用图模式来表达对象之间的关系,在需要进行复杂关系型查询时比较方便,此外ent提供了更加详细的数据类型,适合中大型项目。
官方表达如下:

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的正反关联。

这里car->user的唯一性由Unique()方法保证。
至此我们已经实现了edge设置下的表结构创建和数据写入,接下来我们示例通过edge的查询。

四、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。

6. ent优点和缺点是什么?
先说优点,经过以上示例能发现ent的优点在于:便捷的代码生成手段、完善的仿SQL查询方法、优秀的静态数据类型支持、强大的关系表达能力和查询能力。
再说缺点:
首先ent新手点评缺点不那么准确,不过这依然是必须的,有助于加深理解,未来发现有认知错误时慢慢修改即可。
现在能感觉到的ent的缺点有:
1. 大型项目中会引入过多的edge/inverse edge,导致数据库外键过多,数据量庞大时维护会比较困难。
2. 毫无疑问ent希望我们依靠edge来将schemas全部关联起来,这就要求我们要很好的把握和维护graph关系图,大型项目中这不是一件简单的事情。关系一旦复杂起来维护和重构直接火葬场,当然这点有待实际的项目体验和反馈。
3. 如果彻底放弃edge定义的话,我们的关联查询就需要裸SQL的入口,这和使用标准库的sql包相似。

所以如何定义edge和使用裸SQL需要权衡,目前我的想法是优先使用edge,极少数特殊场景下可以使用裸SQL获取数据。不过这种东西就和mybatis vs hibernate一样争论不休。

六、最后

本篇笔记只是描述ent的快速上手流程,更多ent特性,例如事务支持、完整的CRUD实例和过滤|聚合|分页等查询、GraphQL支持、自定义模板等功能还有待发掘。

 

posted @ 2022-09-01 17:38  realcp1018  阅读(4467)  评论(0编辑  收藏  举报