1524 字
8 分钟
GO项目ORM框架对比与选择
环境搭建
通过docker部署一个mysql8.0容器,然后在里面创建三个db,用来快速进行实现gorm、xorm、ent三种社区常用的orm框架对比。
部署Mysql Docker容器
# 直接运行MySQL 8.0容器,不挂载任何数据卷docker run -d \ --name mysql-orm-test \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -e MYSQL_DATABASE=orm_base \ m.daocloud.io/docker.io/library/mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci \ --max_connections=1000
# 验证运行docker ps | grep mysql创建三个隔离的数据库
# 进入MySQL容器执行命令docker exec -it mysql-orm-test mysql -uroot -proot123
# 在MySQL中执行以下SQL创建三个数据库CREATE DATABASE IF NOT EXISTS gorm_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;CREATE DATABASE IF NOT EXISTS xorm_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;CREATE DATABASE IF NOT EXISTS ent_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
# 查看创建的数据库SHOW DATABASES LIKE '%demo%';测试场景:用户与文章(两张表)
这里的建表语句简单看看即可,在三种框架对比时,会分别用框架迁移能力实现表的创建。
-- 表1: usersCREATE TABLE users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_username (username));
-- 表2: posts (关联users表)CREATE TABLE posts ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, title VARCHAR(200) NOT NULL, content TEXT, view_count INT DEFAULT 0, is_published BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, INDEX idx_user_id (user_id), INDEX idx_created_at (created_at));如果手动创建,则需要分别为三个database去创建这两张表,太繁琐。
三个框架的DEMO实现
项目结构
orm-demo/├── gorm-demo/ # GORM实现│ ├── go.mod│ ├── main.go│ └── internal/├── xorm-demo/ # XORM实现│ ├── go.mod│ └── main.go├── ent-demo/ # Ent实现│ ├── go.mod│ └── ent/└── benchmark/ # 性能测试 ├── main.go └── runners/GORM实现(使用gorm_demo数据库)
package main
import ( "fmt" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" "log" "time")
type User struct { ID uint `gorm:"primaryKey"` Username string `gorm:"size:50;uniqueIndex"` Email string `gorm:"size:100;uniqueIndex"` Posts []Post `gorm:"foreignKey:UserID"` CreatedAt time.Time UpdatedAt time.Time}
type Post struct { ID uint `gorm:"primaryKey"` UserID uint `gorm:"index"` User User `gorm:"foreignKey:UserID"` Title string `gorm:"size:200"` Content string `gorm:"type:text"` ViewCount int `gorm:"default:0"` IsPublished bool `gorm:"default:true"` CreatedAt time.Time}
func main() { // 连接到gorm_demo数据库 dsn := "root:root123@tcp(localhost:3306)/gorm_demo?charset=utf8mb4&parseTime=True&loc=Local" db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Info), }) if err != nil { log.Fatal("连接数据库失败:", err) }
// 自动迁移(创建表) db.AutoMigrate(&User{}, &Post{})
fmt.Println("GORM demo 数据库已初始化")
// 演示CRUD操作 demoCRUDOperations(db)}
func demoCRUDOperations(db *gorm.DB) { // 创建用户 user := User{ Username: "john_doe", Email: "john@example.com", } db.Create(&user)
// 创建关联文章 post := Post{ UserID: user.ID, Title: "GORM使用指南", Content: "这是一个关于GORM的教程...", ViewCount: 100, } db.Create(&post)
// 查询用户及其文章(预加载) var fetchedUser User db.Preload("Posts").First(&fetchedUser, "username = ?", "john_doe") fmt.Printf("用户: %s, 文章数: %d\n", fetchedUser.Username, len(fetchedUser.Posts))}XORM实现(使用xorm_demo数据库)
package main
import ( "fmt" _ "github.com/go-sql-driver/mysql" "log" "time" "xorm.io/xorm" "xorm.io/xorm/names")
type User struct { ID int64 `xorm:"'id' pk autoincr"` Username string `xorm:"'username' varchar(50) unique notnull"` Email string `xorm:"'email' varchar(100) unique notnull"` CreatedAt time.Time `xorm:"'created_at' created"` UpdatedAt time.Time `xorm:"'updated_at' updated"`}
type Post struct { ID int64 `xorm:"'id' pk autoincr"` UserID int64 `xorm:"'user_id' index notnull"` Title string `xorm:"'title' varchar(200) notnull"` Content string `xorm:"'content' text"` ViewCount int `xorm:"'view_count' default 0"` IsPublished bool `xorm:"'is_published' default 1"` CreatedAt time.Time `xorm:"'created_at' created"`}
func (User) TableName() string { return "users"}
func (Post) TableName() string { return "posts"}
func main() { // 连接到xorm_demo数据库 dsn := "root:root123@tcp(localhost:3306)/xorm_demo?charset=utf8mb4&parseTime=true" engine, err := xorm.NewEngine("mysql", dsn) if err != nil { log.Fatal("连接数据库失败:", err) }
// 显示SQL日志 engine.ShowSQL(true)
// 设置表名映射规则 engine.SetMapper(names.GonicMapper{})
// 同步表结构 err = engine.Sync2(new(User), new(Post)) if err != nil { log.Fatal("同步表结构失败:", err) }
fmt.Println("XORM demo 数据库已初始化")
// 演示CRUD操作 demoCRUDOperations(engine)}
func demoCRUDOperations(engine *xorm.Engine) { session := engine.NewSession() defer session.Close()
// 开始事务 session.Begin()
// 创建用户 user := &User{ Username: "jane_smith", Email: "jane@example.com", } _, err := session.Insert(user) if err != nil { session.Rollback() log.Fatal(err) }
// 创建文章 post := &Post{ UserID: user.ID, Title: "XORM性能分析", Content: "XORM是一个简单而强大的ORM...", ViewCount: 150, } _, err = session.Insert(post) if err != nil { session.Rollback() log.Fatal(err) }
// 提交事务 session.Commit()
// 查询用户及其文章 var posts []Post err = engine.Where("user_id = ?", user.ID).Find(&posts) if err != nil { log.Fatal(err) }
fmt.Printf("用户ID: %d, 文章数: %d\n", user.ID, len(posts))}Ent实现(使用ent_demo数据库)
ent命令行工具安装
# 安装ent命令行工具go install entgo.io/ent/cmd/ent@latest
# 查看使用帮助ent --help生成schema初始文件
使用下面的命令生成user和post的初始模板:
ent init User Postent/schema/user.go初始内容:
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}ent/schema/post.go初始内容:
package schema
import "entgo.io/ent"
// Post holds the schema definition for the Post entity.type Post struct { ent.Schema}
// Fields of the Post.func (Post) Fields() []ent.Field { return nil}
// Edges of the Post.func (Post) Edges() []ent.Edge { return nil}在执行完new后,自动生成了user和post的实体,但是Fields方法和Edges都是直接返回nil。
需要在user和post里完成完成字段的定义。
定义schema实体
ent/schema/user.go
package schema
import ( "entgo.io/ent" "entgo.io/ent/schema/field" "entgo.io/ent/schema/edge")
type User struct { ent.Schema}
func (User) Fields() []ent.Field { return []ent.Field{ field.String("username"). Unique(). MaxLen(50), field.String("email"). Unique(). MaxLen(100), }}
func (User) Edges() []ent.Edge { return []ent.Edge{ edge.To("posts", Post.Type), }}ent/schema/post.go
package schema
import ( "entgo.io/ent" "entgo.io/ent/schema/field" "entgo.io/ent/schema/edge")
type Post struct { ent.Schema}
func (Post) Fields() []ent.Field { return []ent.Field{ field.String("title"). MaxLen(200), field.Text("content"), field.Int("view_count"). Default(0), field.Bool("is_published"). Default(true), }}
func (Post) Edges() []ent.Edge { return []ent.Edge{ edge.From("author", User.Type). Ref("posts"). Unique(). Required(), }}生成Ent代码
# 生成代码(需先cd到ent-demo)go generate ./entEnt Demo实现
ent-demo/main.go:
package main
import ( "context" "fmt" "log" "orm-demo/ent-demo/ent" _ "github.com/go-sql-driver/mysql" "orm-demo/ent-demo/ent/user")
func main() { // 连接到ent_demo数据库 dsn := "root:root123@tcp(localhost:3306)/ent_demo?charset=utf8mb4&parseTime=True" client, err := ent.Open("mysql", dsn) if err != nil { log.Fatalf("连接数据库失败: %v", err) } defer client.Close()
ctx := context.Background()
// 运行自动迁移 if err := client.Schema.Create(ctx); err != nil { log.Fatalf("创建Schema失败: %v", err) }
fmt.Println("Ent demo 数据库已初始化")
// 演示CRUD操作 demoCRUDOperations(ctx, client)}
func demoCRUDOperations(ctx context.Context, client *ent.Client) { // 创建用户 userData, err := client.User. Create(). SetUsername("alice_wang"). SetEmail("alice@example.com"). Save(ctx) if err != nil { log.Fatalf("创建用户失败: %v", err) }
// 为用户创建文章 _, err = client.Post. Create(). SetTitle("Ent框架入门"). SetContent("Ent是一个基于代码生成的ORM框架..."). SetViewCount(200). SetAuthor(userData). Save(ctx) if err != nil { log.Fatalf("创建文章失败: %v", err) }
// 查询用户及其文章(预加载) userWithPosts, err := client.User. Query(). Where(user.IDEQ(userData.ID)). WithPosts(). Only(ctx) if err != nil { log.Fatalf("查询用户失败: %v", err) }
// 获取关联的文章 posts, err := userWithPosts.QueryPosts().All(ctx) if err != nil { log.Fatalf("查询文章失败: %v", err) }
fmt.Printf("用户: %s, 文章数: %d\n", userWithPosts.Username, len(posts))}部分信息可能已经过时









