ORM框架对比

Java中常见的ORM框架的基本使用和特点对比

ORM框架对比

语雀同步地址

demo项目地址

前言

本文主要对比的是一下几个 ORM (严格来说,感觉更像是SQL模板引擎)框架,这几个框架,他们都不属于JPA框架,但是在实际中却使用广泛。和JPA这类的ORM框架相比,这些框架更侧重于SQL的原生能力,更为轻量,开发更为简单。

  • Mybatis
  • Mybatis Plus
  • Mybatis Dynamic SQL
  • jOOQ

JPA是由 JCP 组织发布的 Java EE 标准之一(并非具体实现),全称 Java Persistance API 。作为规范,JPA更关注的是模型之间关系,致力于屏蔽原生SQL。JPA最开始的对象关系映射模型基于Hibernate,JPA的第一版很多设计和规范,都是吸收了Hibernate的设计。 除Hibernate外,TopLink、OpenJPA都很好地实现了JPA接口。其中,Hibernate依然是现在最成熟的 JPA框架,获得Sun的兼容认证。 可以说,上面四个框架,最开始的设计思路,与JPA、JDO( Java Data Objects),可谓南辕北辙。很难说这类JPA框架,和以Mybatis为代表的ORM框架孰优孰劣,本文也不在此讨论此问题,下面着重分析下这四款’SQL’类的ORM框架的使用和优缺点。

对比

下面是Mybatis、Mybatis-Plus、Mybatis-Dynamic-SQL、jOOQ的四个简单使用使用。

Mybatis

项目地址

基本使用

  1. 基于注解的mapper
package org.mybatis.example;
public interface BlogMapper {
  @Select("SELECT * FROM blog WHERE id = #{id}")
  Blog selectBlog(int id);
}
  1. 基于xml的mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

动态SQL

官方文档地址: https://mybatis.org/mybatis-3/dynamic-sql.html 支持if、choose、trim、set、foreach,甚至可以支持在注解里使用_script_
和mybatis-dynamic-sql 比起来,这就是个虚假的动态SQL

  1. if条件的动态SQL
<select id="findActiveBlogWithTitleLike"
     resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
</select>
  1. 在注解中使用_script_
 @Update({"<script>",
      "update Author",
      "  <set>",
      "    <if test='username != null'>username=#{username},</if>",
      "    <if test='password != null'>password=#{password},</if>",
      "    <if test='email != null'>email=#{email},</if>",
      "    <if test='bio != null'>bio=#{bio}</if>",
      "  </set>",
      "where id=#{id}",
      "</script>"})
 void updateAuthorValues(Author author);

Mybatis Dynamic SQL

项目地址:

https://github.com/mybatis/mybatis-dynamic-sql

基本使用:

在引入mybatis-3的依赖后,引入mybatis-dynamic-sql依赖和逆向插件

        <dependency>
            <groupId>org.mybatis.dynamic-sql</groupId>
            <artifactId>mybatis-dynamic-sql</artifactId>
            <version>1.2.1</version>
        </dependency>

        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.4.0</version>

            <configuration>
                <configurationFile>src/main/resources/mybatis-generator.xml</configurationFile>
                <overwrite>true</overwrite>
            </configuration>

            <dependencies>
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>8.0.12</version>
                </dependency>
            </dependencies>
        </plugin>

逆向的配置可以参考: http://mybatis.org/generator/quickstart.html

<!DOCTYPE generatorConfiguration PUBLIC
        "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <context id="dsql" targetRuntime="MyBatis3DynamicSql">
        <jdbcConnection
                driverClass="com.mysql.jdbc.Driver"
                connectionURL="jdbc:mysql://127.0.0.1:3306/test"
                userId="root"
                password="123456" />


        <javaModelGenerator targetPackage="com.mybatis.demo.dsl.entity" targetProject="src/main/java"/>

        <javaClientGenerator targetPackage="com.mybatis.demo.dsl.mapper" targetProject="src/main/java" type="ANNOTATEDMAPPER"/>

        <table tableName="dynamic_user" />
    </context>
</generatorConfiguration>

使用示例代码

    public List<DynamicUser> findByLimitAndOffset(long limit, long offset) {
        SelectStatementProvider provider = select(dynamicUser.allColumns())
                .from(dynamicUser)
                .limit(limit)
                .offset(offset)
                .build()
                .render(RenderingStrategies.MYBATIS3);
        return dynamicUserMapper.selectMany(provider);
    }

Mybatis Plus

项目地址:

https://github.com/baomidou/mybatis-plus

基本使用

引入相关依赖后,Mapper接口继承自 BaseMapper 接口即可。Mybatis-Plus也提供了自动生成TABLE的自动化工具(没找到plugin),此处就暂时不引入了。

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testSelectByPrimaryKey() {
        User user = userMapper.selectById(3);
        Assertions.assertNotNull(user, "could not found user");
        log.info("user:{}", JSON.toJSONString(user));
    }

    @Test
    public void testQuery() {
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.select("id", "name", "age", "email").ge("age", 21);
        List<User> users = userMapper.selectList(userQueryWrapper);
        Assertions.assertTrue(users.size() == 3, "查询数量不符!");
        log.info("user:{}", JSON.toJSONString(users));
    }

框架分析

代码分析基于mybatis-plus 3.4.1版本

Mybatis-plus自己定义了一个继承自Mybatis的 org.apache.ibatis.session.SqlSessionFactoryBuilder 自定义的 SqlSessionFactoryBuild : com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder 用以构造自己的SqlSessionFactory 。 在注册Mapper的地方,也使用了自定义的MapperRegistry com.baomidou.mybatisplus.core.MybatisMapperRegistry ,然后用自己的 com.baomidou.mybatisplus.core.MybatisMapperAnnotationBuilder 解析 MybatisMapperAnnotationBuilder 这个类就是注册mapper过程中的关键,它为没有xml的mapper接口注入了基础的CRUD方法。 image.png 对比Mybatis的 org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse 方法,Mybatis-Plus在解析时主要就是红框部分的内容。在 parseInjector() 中循环注入了基础的CRUD方法。 下面的是默认会注入的方法:

public class DefaultSqlInjector extends AbstractSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        return Stream.of(
            new Insert(),
            new Delete(),
            new DeleteByMap(),
            new DeleteById(),
            new DeleteBatchByIds(),
            new Update(),
            new UpdateById(),
            new SelectById(),
            new SelectBatchByIds(),
            new SelectByMap(),
            new SelectOne(),
            new SelectCount(),
            new SelectMaps(),
            new SelectMapsPage(),
            new SelectObjs(),
            new SelectList(),
            new SelectPage()
        ).collect(toList());
    }
}

对比Mybatis的 org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse 方法,Mybatis-Plus在解析时主要就是红框部分的内容。在 parseInjector() 中循环注入了基础的CRUD方法。

下面的是默认会注入的方法:

public class DefaultSqlInjector extends AbstractSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        return Stream.of(
            new Insert(),
            new Delete(),
            new DeleteByMap(),
            new DeleteById(),
            new DeleteBatchByIds(),
            new Update(),
            new UpdateById(),
            new SelectById(),
            new SelectBatchByIds(),
            new SelectByMap(),
            new SelectOne(),
            new SelectCount(),
            new SelectMaps(),
            new SelectMapsPage(),
            new SelectObjs(),
            new SelectList(),
            new SelectPage()
        ).collect(toList());
    }
}

jOOQ

项目地址

https://github.com/jOOQ/jOOQ

官方文档

文档地址: https://www.jooq.org/doc/latest/manual

逆向配置地址: https://www.jooq.org/doc/latest/manual/code-generation/codegen-configuration/

基本使用

生成代码配置

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<configuration xmlns="http://www.jooq.org/xsd/jooq-codegen-3.14.0.xsd">
    <!-- Configure the database connection here -->
    <jdbc>
        <driver>com.mysql.cj.jdbc.Driver</driver>
        <url>jdbc:mysql://localhost:3306/test</url>
        <user>root</user>
        <password>123456</password>
    </jdbc>

    <generator>
        <!-- The default code generator. You can override this one, to generate your own code style.
             Supported generators:
             - org.jooq.codegen.JavaGenerator
             - org.jooq.codegen.KotlinGenerator
             - org.jooq.codegen.ScalaGenerator
             Defaults to org.jooq.codegen.JavaGenerator -->
        <name>org.jooq.codegen.JavaGenerator</name>

        <database>
            <!-- The database type. The format here is:
                 org.jooq.meta.[database].[database]Database -->
            <name>org.jooq.meta.mysql.MySQLDatabase</name>

            <!-- The database schema (or in the absence of schema support, in your RDBMS this
                 can be the owner, user, database name) to be generated -->
            <inputSchema>test</inputSchema>

            <!-- All elements that are generated from your schema
                 (A Java regular expression. Use the pipe to separate several expressions)
                 Watch out for case-sensitivity. Depending on your database, this might be important! -->
            <includes>dynamic_user</includes>

            <!-- All elements that are excluded from your schema
                 (A Java regular expression. Use the pipe to separate several expressions).
                 Excludes match before includes, i.e. excludes have a higher priority -->
            <!--<excludes>.*</excludes>-->
        </database>

        <target>
            <!-- The destination package of your generated classes (within the destination directory) -->
            <packageName>com.jooq.demo.db</packageName>

            <!-- The destination directory of your generated classes. Using Maven directory layout here -->
            <directory>./src/main/java</directory>
        </target>
    </generator>
</configuration>

插件配置:

            <plugin>
                <groupId>org.jooq</groupId>
                <artifactId>jooq-codegen-maven</artifactId>
                <version>3.14.4</version>
                <executions>
                    <execution>
                        <id>jooq-codegen</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <configurationFile>src/main/resources/jooq-codegen.xml</configurationFile>
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>

使用demo

    @Autowired
    private DSLContext dsl;

    public int saveUser(String name, Byte gender) {
        return dsl.insertInto(DYNAMIC_USER)
                .set(DYNAMIC_USER.NAME, name)
                .set(DYNAMIC_USER.GENDER, gender)
                .execute();
    }

    public List<DynamicUserRecord> pageQuery(String name, Byte gender, int offset, int limit) {
        Condition condition = DSL.noCondition();
        if (!StringUtils.isBlank(name)) {
            condition.and(DYNAMIC_USER.NAME.eq(name));
        }
        if (gender != null) {
            condition.and(DYNAMIC_USER.GENDER.eq(gender));
        }
        return dsl.selectFrom(DYNAMIC_USER)
                .where(condition)
                .offset(offset)
                .limit(limit)
                .fetchInto(DynamicUserRecord.class);
    }

    public DynamicUserRecord selectById(Integer id) {
        return dsl.selectFrom(DYNAMIC_USER)
                .where(DYNAMIC_USER.ID.eq(id))
                .fetchAny();
    }

    public int countUserByGender(Byte gender) {
        return dsl.fetchCount(DYNAMIC_USER, DYNAMIC_USER.GENDER.eq(gender));
    }

总结

上述的集中框架,除jOOQ外,Mybatis Plus和Mybatis Dynamic SQL都是完美兼容Mybatis(更不如说是Mybatis的扩展和延伸)。不过,Mybatis Plus主要的功能,就是在原生Mybatis的基础上,为mapper增加了通用的CRUD方法,如果需要join或者复杂查询,依然是写xml或者注解的方式。Mybatis Plus和Mybatis可以近似理解为类似于Spring Data JPA和JPA两者的关系,所以,Mybatis Plus在此就不多做讨论。

下面着重讨论下两个SQL生成器:Mybatis Dynamic SQL 和 jOOQ。

几款框架的对比

相同点:

  • 两者都引入了 DSL( Domain Specific Language)风格,丰富的Fluent API,代码逻辑清晰,学习成本不高
  • 注重于原生的SQL能力,关注点在SQL的生成、结果的映射,排查问题成本相对较低。

不同点:

  • Mybatis Dynamic SQL出自Mybatis,还特地为Mybatis提供了类似于 CommonCountMapper 、CommonDeleteMapper、 CommonInsertMapper 这些支持
  • Mybatis Dynamic SQL相对jOOQ更轻量一些
  • jOOQ社区相对更加活跃,资料也比较多,DSL API 的功能更丰富一些

个人观点

Mybatis最原始XML Mapper的方式操作数据库,是注定会被淘汰的。而JPA框架,隐藏了所有细节,对程序员可见的只有一个Java Bean,当需要排查问题时,可能会比较麻烦。

类似jOOQ和Mybatis Dynamic SQL这两种DSL框架,更专注于SQL生成,少了一层XML,维护起来更方便。

对比jOOQ个Mybatis Dynamic SQL两个框架,单纯从 SQL generator 的角度看的话,个人观点 Mybatis Dynamic SQL 会更简单、更纯粹一些。不过无论是 jOOQ还是 Mybatis Dynamic SQL,这两者网上的中文资料还是偏少,特别是 Mybtais Dynamic SQL,所以如果要考虑技术选型的话,这也是一个需要关注的点。