ORM哪家强?java,c#,php,python,go 逐一对比, 网友直呼:全面客观

前言

最近一段时间,我使用golang开发了一个新的ORM库。

为了让这个库更好用,我比较研究了各语言的主流ORM库,发现有一些语言的ORM库确实很好用,而有另外一些语言的库那不是一般的难用。

然后我总结了他们呢的一些共性和差异点,于是形成了本文的主要内容。

本文会先说明什么是SQL编写难题,以及探讨一下 code firstdatabase first 的优缺点。
然后依据这两个问题的结论去审视目前主流后端语言java, c#, php, python, go各自的orm库,对比研究下他们的优缺点。最后给出总结和参考文档。

如果你需要做技术选型,或者做技术研究,或者类似于我做框架开发,或者单纯地了解各语言的差异,或者就是想吹个牛,建议保存或收藏。如果本文所涉及到的内容有任何不正确,欢迎批评指正。

温馨提示,本文会有一些戏谑或者调侃成分,并非对某些语言或者语言的使用者有任何歧视意见。
如果对你造成了某些伤害,请多包涵。

renzhen.png

什么是SQL编写难题

如果你是做web开发,那么必然需要保存数据到数据库,这个时候你必须熟悉使用sql语句来读写数据库。

sql本身不难,命令也就那几个,关键字也不算多,但是为什么编写sql会成为难题呢?

比如下面的sql

    select * from user
    
    insert user (name,mobile) values ('tang','18600000000')

它有什么难题? 简单的单表操作嘛,一点难题没有,但凡学过点sql的程序员都能写出来,并且保证正确。我估计比例能超过90%

但是,如果你需要写下面的sql呢?

    SELECT 
        article.*,
        person.name as person_name 
    FROM article 
    LEFT JOIN person ON person.id=article.person_id 
    WHERE article.type = 0 
    AND article.age IN (18,20)

这个也不复杂,就是你在做查询列表的时候,会经常用到的联表查询。你是否还有勇气说,写出来的sql绝对正确。我估计比例不超过70%

再稍微复杂点,如果是下面的sql?

    SELECT 
        o.*,
        d.department_name,
        (SELECT Sum(so.goods_fee) AS task_detail_target_completed_tem 
         FROM sale_order so 
         WHERE so.merchant_id = '356469725829664768' 
         AND so.create_date BETWEEN (20230127) AND (20230212) 
         AND so.delete_state = 2 
         AND so.department_id = o.department_id
        ) AS task_detail_target_completed 
    FROM task_detail o 
    LEFT JOIN department d ON d.department_id=o.department_id 
    WHERE o.merchant_id = '356469725829664768' 
    AND o.task_id = '356469725972271104768'

这是我项目里真实的sql语句,目的是统计出所有部门在某时间段内各自的业绩。逻辑上也不太复杂,但你是否还有勇气说,写出来的sql绝对正确。我估计比例不超过40%

如上面的sql所示,SQL编写难题在于以下几方面。

要保证字段正确

应该有的字段不能少,不应该有的字段不能多。

比如你把mobile误打成mobike,这属于拼写错误,但是这个拼写错误只有在实际运行的时候才会告诉你字段名错了。

并且项目越大,表越多,字段越多,这种拼写错误发生的可能性越大。以至于可以肯定的说,100%的可能性会出现。

要特别注意sql语法

例如你在查询的时候必须写from,绝对不能误写成form,但是在实际开发过程中,很容易就打错了。

这种错误,也只有运行的时候才会告诉你语法错了。并且sql越复杂,这种语法错误发生的可能性越大。

编辑器不会有sql的语法提示

常见的编码用的软件,对于sql相关的代码,不会有语法提示,也不会有表名提示,字段名提示。

最终的代码质量如何全凭你的眼力,经验,能力。

下载.png

很显然,既然存在该难题,那么哪个ORM能解决该难题,就应该算得上好,如果不能解决,则不能称之为好。

什么是code first 和 database first

这俩概念并不是新概念,但是我估计大多数开发者并不熟悉。

所谓 code first, 相近的词是 model fist, 意思是模型优先,指的是在设计和开发系统时,优先和重点做的工作是设计业务模型,然后根据业务模型去创建数据库。

所谓 database first,意思是数据库优先,指的是在设计和开发系统时,优先和重点做的工作是创建数据库结构,然后去实现业务。

这里我提到了几个词语,可能在不同的语言里叫法不一样,可能不同的人的叫法也不一样,为了下述方便,我们举例子来说。

code first 例子

假设我是一个对电商系统完全不懂的小白,手头上也没有如何设计电商系统的资料,我和我的伙伴只是模糊地知道电商系统主要业务就是处理订单。

然后我大概会知道这个订单,主要的信息包括哪个用户下单,什么时间下单,有哪几种商品,数量分别是多少,根据这些已有的信息,我可以设计出来业务模型如下

public class OrderModel {
    //订单编号
    Integer orderId;
    //用户编号
    Integer userId;
    //订单时间
    Integer createTime;
    //订单详情(包含商品编号,商品数量)
    String  orderDetail;
}

很简单,对吧,这个模型很匹配我目前对系统的认知。接下来会做各种业务逻辑,最后要做的是将订单模型的数据保存到数据库。但是在保存数据到数据库的时候,就有一些考虑了。

我可以将上面OrderModel业务模型建立一张对应表,里面的4个属性,对应数据表里的4个字段,这完全可以。
但是我是电商小白,不是数据库小白啊,这样存储的话,肯定不利于统计订单商品的。

所以我换一种策略,将OrderModel的信息进行拆分,将前三个属性 orderId, userId, createTime 放到一个新的类里。
然后将 orderDetail 的信息进行再次分解,放到另一个类里

public class OrderEntity {
    Integer orderId;
    Integer userId;
    Integer createTime;
}

public class OrderDetailEntity {
    Integer orderDetailId;
    Integer orderId;
    Integer goodsId;
    Integer goodsCount;
}

最后,在数据库建立两张表order,order_detail,表结构分别对应类OrderEntity,OrderDetailEntity的结构。

至此,我们完成了从业务模型OrderModel到数据表order,order_detail的过程。

这就是 code first ,注意这个过程的关键点,我优先考虑的是模型和业务实现,后面将业务模型数据进行分解和保存是次要的,非优先的。

database first 例子

假设我是一个对电商系统非常熟悉的老鸟,之前做过很多电商系统,那么我在做新的电商系统的时候,就完全可以先设计数据库。

order表放订单主要数据,里面有xxx几个字段,分别有什么作用,有哪些状态值

order_detail表放订单详情数据,,里面有xxx几个字段,分别有什么作用

这些都可以很清楚和明确。然后根据表信息,生成OrderEntity,以及OrderDetailEntity即可开始接下来的编码工作。这种情况下OrderModel可能有,也可能没有。

这就是 database first ,注意这个过程的关键点,我优先考虑的是数据库结构和数据表结构。

两种方式对比

code first 模式下, 系统设计者优先考虑的是业务模型OrderModel, 它可以描述清楚一个完整业务,包括它的所有业务细节(什么人的订单,什么时候的订单,订单包含哪些商品,数量多少),有利于设计者对于系统的整体把控。

database first 模式下, 系统设计者优先考虑的是数据表order,order_detail,他们中任何一张表都不能完整的描述清楚一个完整业务,只能够描述局部细节,不利于设计者对于系统的整体把控。

在这里,调皮的同学会问,在 database first 模式下, 我把order,order_detail的信息一起看,不就知道完整的业务细节了吗?

确实是这样,但这里有一个前提,前提是你必须明确的知道order,order_detail是需要一起看的,而你知道他们需要一起看的前提是你了解电商系统。 如果你设计的不是电商系统,而是电路系统,你还了解吗?还知道哪些表需要一起看吗?

至此,我们可以有以下粗浅的判断:

对于新项目,不熟悉的业务,code first 模式更适合一些

对于老项目,熟悉的业务,database first 模式更合适一些

如果两种模式都可以的话,优先使用 code first 模式,便于理解业务,把控项目

如果哪个ORM支持 code first , 我们可以稍稍认为它更好一些

Java体系的orm

Java语言是web开发领域处于领先地位,这一点无可置疑。它的优点很明显,但是缺点也不是没有。

国内应用比较广泛的orm是Mybatis,以及衍生品Mybatis-plus等

实际上Mybatis团队还出了另外一款产品,MyBatis Dynamic SQL,国内我见用的不多,讨论都较少。英文还可以的同学,可以看下面的文档。

另外还有 jOOQ, 实际上跟 MyBatis Dynamic SQL 非常类似,有兴趣的可以去翻翻

下面,我们举一些例子,来对比一下他们的基本操作

Java体系的Mybatis

单就orm这一块,国内用的最多的应该是Mybatis,说到它的使用体验吧,那简直是一言难尽。

你需要先定义模型,然后编写xml文件用来映射数据,然后创建mapper文件,用来执行xml里定于的sql。
从这个流程可以看出,中间的xml文件起到核心作用,里面不光有数据类型转换,还有最核心的sql语句。

典型的xml文件内容如下

<mapper namespace="xxx.mapper.UserMapper">
    <insert id="insertUser" parameterType="UserEntity">
        insert into user (id,name,mobile)
        values (#{id},#{name},#{mobile})
    </insert>

    <update id="updateUser" parameterType="UserEntity">
        update user set
        name = #{name},
        mobile = #{mobile}
        where id = #{id}
    </update>

    <delete id="deleteUser">
        delete from user where id = #{id}
    </delete>

    <select id="selectUsers" resultType="UserVO">
        select u.*, (select count(*) from article a where a.uid=u.id) as article_count
        from user u
        where u.id = #{id}
    </select>
</mapper>

你在编写这个xml文件的时候,这个手写sql没有本质区别,一定会遇到刚才说到的SQL编写难题

Java体系的Mybatis-plus

这里有必要提一下 Mybatis-plus,它是国内的团队开发出来的工具,算是对Mybatis的扩展吧,它减少了xml文件内容的编写,减少了一些开发的痛苦。比如,你可以使用如下的代码来完成以上相同的工作

    userService.insert(user);

    userService.update(user);

    userService.deleteById(user);

    List<UserEntity> userList = userService.selectList(queryWrapper);

完成这些工作,你不需要编写任何xml文件,也不需要编写sql语句,如之前所述,减少了一些开发的痛苦。

但是,请你注意我的用词,是减少了一些。

对于连表操作,嵌套查询等涉及到多表操作的事情,它就不行了,为啥不行,因为根本就不支持啊。
遇到这种情况,你就老老实实的去写xml吧,然后你还会遇到刚才说到的SQL编写难题

Java体系的Mybatis3 Dynamic Sql

值得一提的是Mybatis3 Dynamic Sql,翻译一下就是动态sql。还是刚才说的国内我见用的不多,讨论都较少,但是评价看上去挺好。

简单来说,可以根据不同条件拼接出sql语句。不同于上面的Mybatis,这些sql语句是程序运行时生成的,而不是提前写好的,或者定义好的。

它的使用流程是,先在数据库里定义好数据表,然后创建模型文件,让然后通过命令行工具,将每一个表生成如下的支持文件

public final class PersonDynamicSqlSupport {
    public static final Person person = new Person();
    public static final SqlColumn<Integer> id = person.id;
    public static final SqlColumn<String> firstName = person.firstName;
    public static final SqlColumn<LastName> lastName = person.lastName;
    public static final SqlColumn<Date> birthDate = person.birthDate;
    public static final SqlColumn<Boolean> employed = person.employed;
    public static final SqlColumn<String> occupation = person.occupation;
    public static final SqlColumn<Integer> addressId = person.addressId;

    public static final class Person extends SqlTable {
        public final SqlColumn<Integer> id = column("id", JDBCType.INTEGER);
        public final SqlColumn<String> firstName = column("first_name", JDBCType.VARCHAR);
        public final SqlColumn<LastName> lastName = column("last_name", JDBCType.VARCHAR, "examples.simple.LastNameTypeHandler");
        public final SqlColumn<Date> birthDate = column("birth_date", JDBCType.DATE);
        public final SqlColumn<Boolean> employed = column("employed", JDBCType.VARCHAR, "examples.simple.YesNoTypeHandler");
        public final SqlColumn<String> occupation = column("occupation", JDBCType.VARCHAR);
        public final SqlColumn<Integer> addressId = column("address_id", JDBCType.INTEGER);

        public Person() {
            super("Person");
        }
    }
}

可以看出,这里的主要功能能是将表内的字段,与java项目里的类里面的属性,做了一一映射。

接下来你在开发的时候,就不用关心表名,以及字段名了,直接使用刚才生成的类,以及类下面的那些属性。具体如下

        SelectStatementProvider selectStatement = select(id.as("A_ID"), firstName, lastName, birthDate, employed,occupation, addressId)
        .from(person)
        .where(id, isEqualTo(1))
        .or(occupation, isNull())
        .build()
        .render(RenderingStrategies.MYBATIS3);

        List<PersonRecord> rows = mapper.selectMany(selectStatement);

如上面的代码,好处有以下四点

  1. 你不再需要手写sql
  2. 也不用在意字段名了,因为使用的都是类,或者属性,编写代码的时候编辑器会有提示,编译的时候如果有错误也会提示,实际运行的时候就不会有问题了。
  3. 联表查询,嵌套查询啥的,也都支持
  4. 完美避开了SQL编写难题

当然带来了额外的事情,比如你要使用工具来生成PersonDynamicSqlSupport类,比如你要先建表。

先建表这事儿,很明显就属于 database first 模式。

事情不大.png

C#体系的orm

C# 在工业领域,游戏领域用的多一些,在web领域少一些。

它也有自己的orm,名字叫 Entity Framework Core, 一直都是微软公司在维护。

下面是一个典型的联表查询

    var id = 1;
    var query = database.Posts
                .Join(database.Post_Metas,
                    post => post.ID,
                    meta => meta.Post_ID,
                    (post, meta) => new { Post = post, Meta = meta }
                )
                .Where(postAndMeta => postAndMeta.Post.ID == id);

这句代码的主要作用是,将数据库里的Posts表,与Post_Metas表做内联操作,然后取出Post.ID等于1的数据

这里出现的Post,以及Meta都是提前定义好的模型,也就是类。 Post.ID 是 Post 的一个属性,也是提前定义好的。

整个功能的优点很多,你不再需要手写sql,不需要关心字段名,不需要生成额外类,也不会有语法错误,你只需要提前定义好模型,完全没有SQL编写难题,很明显就属于 code first 模式。

对比java的Mybatis以及Mybatis3 Dynamic Sql来说,你可以脑补一下下面的场景

javavsc#.png

PHP体系的orm

php体系内,框架也非常多,比如常见的laravel,symfony,这里我们就看这两个,比较有代表性

PHP体系的laravel

使用php语言开发web应用的也很多,其中比较出名的是laravel框架,比较典型的操作数据库的代码如下

$user = DB::table('users')->where('name', 'John')->first();

这里没有使用模型(就算使用了也差不多),代码里出现的 users 就是数据库表的名字, name 是 users 表里的字段名,他们是被直接写入代码的

很明显它会产生SQL编写难题

并且,因为是先设计数据库,肯定也属于 database first 模式

PHP体系的symfony

这个框架历史也比较悠久了,它使用了 Doctrine 找个类库作为orm

使用它之前,也需要先定义模型,然后生成支持文件,然后建表,但是在实际使用的时候,还是和laravel一样,表名,字段名都需要硬编码

$repository = $this->getDoctrine()->getRepository('AppBundle:Product');
 
// query for a single product by its primary key (usually "id")
// 通过主键(通常是id)查询一件产品
$product = $repository->find($productId);
 
// dynamic method names to find a single product based on a column value
// 动态方法名称,基于字段的值来找到一件产品
$product = $repository->findOneById($productId);
$product = $repository->findOneByName('Keyboard');

// query for multiple products matching the given name, ordered by price
// 查询多件产品,要匹配给定的名称和价格
$products = $repository->findBy(
    array('name' => 'Keyboard'),
    array('price' => 'ASC')
);

很明显它也会产生SQL编写难题

另外,并不是先设计表,属于 code first 模式

下载.png

python体系的orm

在python领域,有一个非常著名的框架,叫django, 另外一个比较出名的叫flask, 前者追求大而全,后者追求小而精

python体系的django

django推荐的开发方法,也是先建模型,但是在查询的时候,这建立的模型,基本上毫无用处

    res=models.Author.objects.filter(name='jason').values('author_detail__phone','name')
    print(res)
    # 反向
    res = models.AuthorDetail.objects.filter(author__name='jason')  # 拿作者姓名是jason的作者详情
    res = models.AuthorDetail.objects.filter(author__name='jason').values('phone','author__name')
    print(res)


    # 2.查询书籍主键为1的出版社名称和书的名称
    res = models.Book.objects.filter(pk=1).values('title','publish__name')
    print(res)
    # 反向
    res = models.Publish.objects.filter(book__id=1).values('name','book__title')
    print(res)

如上连表查询的代码,values('title','publish__name') 这里面写的全都是字段名,硬编码进去,进而产生sql语句,查询出结果

很显然,它也会产生SQL编写难题

另外,并不是先设计表,属于 code first 模式

python体系的flask

flask本身没有orm,一般搭配 sqlalchemy 使用

使用 sqlalchemy 的时候,一般也是先建模型,然后查询的时候,可以直接使用模型的属性,而无须硬编码

result = session.               
query(User.username,func.count(Article.id)).
join(Article,User.id==Article.uid).
group_by(User.id).
order_by(func.count(Article.id).desc()).
all()

如上 Article.id 即是 Article 模型下的 id 属性

很显然,它不会产生SQL编写难题

另外,并不是先设计表,属于 code first 模式

下载 (1).png

go体系的orm

在go体系,orm比较多,属于百花齐放的形态,比如国内用的多得gorm以及gorm gen,国外比较多的ent, 当然还有我自己写的 arom

go体系下的gorm

使用gorm,一般的流程是你先建立模型,然后使用类似如下的代码进行操作

type User struct {
  Id  int
  Age int
}

type Order struct {
  UserId     int
  FinishedAt *time.Time
}

query := db.Table("order").
Select("MAX(order.finished_at) as latest").
Joins("left join user user on order.user_id = user.id").
Where("user.age > ?", 18).
Group("order.user_id")

db.Model(&Order{}).
Joins("join (?) q on order.finished_at = q.latest", query).
Scan(&results)

这是一个嵌套查询,虽然定义了模型,但是查询的时候并没有使用模型的属性,而是输入硬编码

很显然,它会产生SQL编写难题

另外,是先设计模型,属于 code first 模式

go体系下的gorm gen

gorm gen 是 gorm 团队开发的另一款产品,和mybaits下的Mybatis3 Dynamic Sql比较像

它的流程是 先创建数据表,然后使用工具生成结构体(类)和支持代码, 然后再使用生成的结构体

它生成的比较关键的代码如下

func newUser(db *gorm.DB) user {
	_user := user{}

	_user.userDo.UseDB(db)
	_user.userDo.UseModel(&model.User{})

	tableName := _user.userDo.TableName()
	_user.ALL = field.NewAsterisk(tableName)
	_user.ID = field.NewInt64(tableName, "id")
	_user.Name = field.NewString(tableName, "name")
	_user.Age = field.NewInt64(tableName, "age")
	_user.Balance = field.NewFloat64(tableName, "balance")
	_user.UpdatedAt = field.NewTime(tableName, "updated_at")
	_user.CreatedAt = field.NewTime(tableName, "created_at")
	_user.DeletedAt = field.NewField(tableName, "deleted_at")
	_user.Address = userHasManyAddress{
		db: db.Session(&gorm.Session{}),

		RelationField: field.NewRelation("Address", "model.Address"),
	}

	_user.fillFieldMap()

	return _user
}

注意看,其中大多数代码的作用是啥?不意外,就是将结构体的属性与表字段做映射关系

_user.Name 对应 name
_user.Age 对应 age

如此,跟mybaits下的Mybatis3 Dynamic Sql的思路非常一致

典型查询代码如下

u := query.User
err := u.WithContext(ctx).
    Select(u.Name, u.Age.Sum().As("total")).
    Group(u.Name).
    Having(u.Name.Eq("group")).
    Scan(&users)

// SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group"

这是一个分组查询,定义了模型,也使用了模型的属性。

但是呢,它需要使用工具生成额外的支持代码,并且需要先定义数据表

很显然,它不会产生SQL编写难题

另外,它是先设计表,属于 database first 模式

go体系下的ent

ent 是 facebook公司开发的Orm产品,与 gorm gen 有相通,也有不同

相同点在于,都是利用工具生成实体与数据表字段的映射关系

不同点在于gorm gen先有表和字段,然后生成实体

ent是没有表和字段,你自己手动配置,配置完了一起生成实体和建表

接下来,看一眼ent生成的映射关系

const (
	// Label holds the string label denoting the user type in the database.
	Label = "user"
	// FieldID holds the string denoting the id field in the database.
	FieldID = "id"
	// FieldName holds the string denoting the name field in the database.
	FieldName = "name"
	// FieldAge holds the string denoting the age field in the database.
	FieldAge = "age"
	// FieldAddress holds the string denoting the address field in the database.
	FieldAddress = "address"
	// Table holds the table name of the user in the database.
	Table = "users"
)

有了映射关系,使用起来就比较简单了

u, err := client.User.
        Query().
        Where(user.Name("realcp")).
        Only(ctx)

注意,这里没有硬编码

它需要使用工具生成额外的支持代码,并且需要先配置表结构

很显然,它不会产生SQL编写难题

另外,它属于先设计表,属于 database first 模式

go体系下的aorm

aorm 是我自己开发的orm库,吸取了ef core 的一些优点,比较核心的步骤如下

和大多数orm一样,需要先建立模型,比如

    type Person struct {
        Id         null.Int    `aorm:"primary;auto_increment" json:"id"`
        Name       null.String `aorm:"size:100;not null;comment:名字" json:"name"`
        Sex        null.Bool   `aorm:"index;comment:性别" json:"sex"`
        Age        null.Int    `aorm:"index;comment:年龄" json:"age"`
        Type       null.Int    `aorm:"index;comment:类型" json:"type"`
        CreateTime null.Time   `aorm:"comment:创建时间" json:"createTime"`
        Money      null.Float  `aorm:"comment:金额" json:"money"`
        Test       null.Float  `aorm:"type:double;comment:测试" json:"test"`
    }
	

然后实例化它,并且保存起来

    //Instantiation the struct
    var person = Person{}
	
    //Store the struct object
    aorm.Store(&person)

然后即可使用

    var personItem Person
    err := aorm.Db(db).Table(&person).WhereEq(&person.Id, 1).OrderBy(&person.Id, builder.Desc).GetOne(&personItem)
    if err != nil {
        fmt.Println(err.Error())
    }

很显然,它不会产生SQL编写难题

另外,它属于先设计模型,属于 code first 模式

下载 (2).png

总结

本文,我们提出了两个衡量orm功能的原则,并且对比了几大主流后端语言的orm,汇总列表如下

框架 语言 SQL编写难题 code first 额外创建文件
MyBatis 3 java 有难度 不是 需要
MyBatis-Plus java 有难度 不是 不需要
MyBatis Dynamic SQL java 没有 不是 需要
jOOQ java 没有 不是 需要
ef core c# 没有 不需要
laravel php 有难度 不是 不需要
symfony php 有难度 不是 需要
django python 有难度 不需要
sqlalchemy python 没有 不需要
grom go 有难度 不需要
grom gen go 没有 不是 需要
ent go 没有 不是 需要
aorm go 没有 不需要

单就从这张表来说,不考虑其他条件,在做orm技术选型时,

如果你使用java语言,请选择 MyBatis Dynamic SQL 或者 jOOQ,因为选择他们不会有SQL编写难题

如果你使用c#语言,请选择 ef core, 这已经是最棒的orm了,不会有SQL编写难题,支持code first,并且不需要额外的工作

如果你使用php语言,请选择 laravel 而不是 symfony, 反正都有SQL编写难题,那就挑个容易使用的

如果你使用python语言,请选择 sqlalchemy 库, 不会有SQL编写难题,支持code first,并且不需要额外的工作

如果你使用go语言,请选择 aorm 库, 不会有SQL编写难题,支持code first,并且不需要额外的工作

好了,文章写两天了,终于写完了。如果对你有帮助,记得点赞,收藏,转发。

如果我有说的不合适,或者不对的地方,请在下面狠狠的批评我。

微信图片_20221226163643.png

参考文档

MyBatis 3
MyBatis-Plus
MyBatis Dynamic SQL
jOOQ: The easiest way to write SQL in Java
Entity Framework Core 概述 - EF Core | Microsoft Learn
数据库和Doctrine ORM - Symfony开源 - Symfony中国 (symfonychina.com)
Django(ORM查询、多表、跨表、子查询、联表查询) - 知乎 (zhihu.com)
Sqlalchemy join连表查询_FightAlita的博客-CSDN博客_sqlalchemy 连表查询
Gorm + Gen自动生成数据库结构体_Onemorelight95的博客-CSDN博客_gorm 自动生成
tangpanqing/aorm: Operate Database So Easy For GoLang Developer (github.com)

posted @ 2023-01-29 16:01  程序员汤汤  阅读(9404)  评论(152编辑  收藏  举报