前言

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

为了让这个库更好用,我比拟钻研了各语言的支流ORM库,发现有一些语言的ORM库的确很好用,而有另外一些语言的库那不是个别的难用。

而后我总结了他们呢的一些共性和差别点,于是造成了本文的次要内容。

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

如果你须要做技术选型,或者做技术钻研,或者相似于我做框架开发,或者单纯地理解各语言的差别,或者就是想吹个牛,倡议保留或珍藏。如果本文所波及到的内容有任何不正确,欢送批评指正。

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

什么是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相干的代码,不会有语法提醒,也不会有表名提醒,字段名提醒。

最终的代码品质如何全凭你的目力,教训,能力。

很显然,既然存在该难题,那么哪个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 模式。

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来说,你能够脑补一下上面的场景

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 模式

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 模式

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.Usererr := 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 模式

总结

本文,咱们提出了两个掂量orm性能的准则,并且比照了几大支流后端语言的orm,汇总列表如下

框架语言SQL编写难题code first额定创立文件
MyBatis 3java有难度不是须要
MyBatis-Plusjava有难度不是不须要
MyBatis Dynamic SQLjava没有不是须要
jOOQjava没有不是须要
ef corec#没有不须要
laravelphp有难度不是不须要
symfonyphp有难度不是须要
djangopython有难度不是不须要
sqlalchemypython没有不须要
gromgo有难度不须要
grom gengo没有不是须要
entgo没有不是须要
aormgo没有不须要

单就从这张表来说,不思考其余条件,在做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,并且不须要额定的工作

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

如果我有说的不适合,或者不对的中央,请在上面狠狠的批评我。

参考文档

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)