文章

MyBatis

mybatis作为一款半自动的orm框架,好处就是黑盒感没有那么强,大部分操作都是由自己写sql来操控的。可以理解为没有hibernate那么强大,也可以理解为学习成本没有hibernate那么高。是利是弊,全取决于自己的倾向和使用场景。

Spring - JDBC & ORM已经提到了mybatis,主要介绍的是mybatis和其他orm框架的区别、事务处理等。

  1. mybatis
    1. 配置
      1. mapper class:顺藤摸瓜
    2. mapper
      1. xml mapper
      2. type handler:类型映射
      3. resultMap:结果映射
        1. 联表查询的映射
        2. 选择type handler
        3. 复杂的resultMap
    3. 使用
      1. mapper class
    4. 其他
      1. intercepter
      2. sql generator
  2. mybatis-spring
    1. bean
    2. @MapperScan
    3. xml mapper的编译问题
    4. 所以@Mapper有什么用?
    5. SqlSessionTemplate
    6. 使用
  3. mybatis-spring-boot-starter
    1. 配置属性
  4. mybatis-plus
    1. 高级特性
  5. mybatis-plus-spring-boot-starter
  6. Test
    1. mybatis-test
    2. mybatis-spring-boot-starter-test
    3. mybatis-plus-test
    4. mybatis-plus-boot-starter-test
  7. 奇奇怪怪的知识
  8. 感想

mybatis

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

优点:

  • 不用写jdbc模板代码;
  • 不用手动转换结果集(自动映射了);

所以轻量的系统很适合使用mybatis。

使用mybatis的步骤:

  1. 配置SqlSessionFactory;
  2. 配置映射;
  3. 使用映射查询;

因为需要单独配置结果集和对象的映射,所以就不需要像jdbc一样做结果转换了。当然大部分情况下,自动映射就够了。

配置

早期的mybatis是使用xml配置的,后来也支持java配置,不过二者在逻辑上是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

主要在配置mybatis本身的一些属性,比如:

  • 是否使用transaction manager;
  • DataSource;
  • 映射文件mapper的位置<mappers>

然后就可以使用该xml初始化SqlSessionFactory

1
2
3
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

当然也可以纯java配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    private static SqlSessionFactory getSqlSessionFactory() {
        DataSource dataSource = getDataSource();

        TransactionFactory transactionFactory = new JdbcTransactionFactory();
        Environment environment = new Environment("development", transactionFactory, dataSource);
        Configuration configuration = new Configuration(environment);

        // 必须通过这种方式添加mapper,除非通过mybatis-spring自动发现并注入mapper
        configuration.addMapper(BlogMapper.class);
        return new SqlSessionFactoryBuilder().build(configuration);
    }

    private static DataSource getDataSource() {
        HikariConfig config = new HikariConfig();
        // 启动h2时初始化sql脚本
        config.setJdbcUrl("jdbc:h2:mem:pokemon;DB_CLOSE_DELAY=-1;MODE=MySQL;INIT=RUNSCRIPT FROM 'classpath:scripts/init.sql'");
        config.setUsername("sa");
        config.setPassword("password");
        config.setDriverClassName("org.h2.Driver");
        return new HikariDataSource(config);
    }

还可以做更细化的配置,比如后面提到的:

  • autoMappingBehavior:指定 MyBatis 应如何自动映射结果集的列到对象属性;
  • mapUnderscoreToCamelCase是否开启驼峰命名自动映射,即从经典数据库列名A_COLUMN映射到经典 Java 属性名aColumn。这个一般是要开启的

mybatis并不能自动扫描mapper的位置,所以要在配置里手动指定mapper的位置。mybatis认为这是Java的锅:D

首先,我们需要告诉 MyBatis 到哪里去找到这些语句。 在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件。

最常用的就是到classpath下找

1
2
3
4
5
6
<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>

绝对路径就比较离谱了:

1
2
3
4
5
6
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>

这个和下文提到的使用mapper class配置xml mapper一个道理。只不过都用Java配置了,很少会再用到xml配置mybatis。所以应该适用于一些遗留的老项目:

1
2
3
4
5
6
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>

类似于auto scan了:

1
2
3
4
<!-- 将包内的映射器接口全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

所以接入springboot好啊,mybatis-spring-boot-autoconfigure就能自动扫描mapper的位置了。

mapper class:顺藤摸瓜

不知道你注意没有:在xml配置mybatis的时候,通过<mapper>指定xml mapper的位置。但是在使用Java代码配置mybatis的时候,只通过configuration.addMapper(BlogMapper.class)设置了BlogMapper,并没有设置它对应的xml mapper文件,这是怎么回事?

仔细看了Configuration#addMapper(Class<T> type)才发现端倪:原来set mapper class大有文章,通过mapper class直接找到了xml mapper的位置……

第一反应我是懵逼的,感觉有点儿隐晦……

mybatis表面上set mapper class,实际通过mapper class获取了xml mapper的位置:xmlResource = type.getName().replace('.', '/') + ".xml",然后按照这个位置从classpath里加载并解析xml……

所以通过Java代码设置mybatis,xml文件必须在classpath上,且和mapper class有相同的目录结构!

使用mapper class还有其他作用:mapper class定义的方法和xml mapper里定义的方法具有同等效力(mybatis称之为statement),所以还要解析mapper class里的method,记录下来。

mapper

不要嫌弃写mapper,毕竟使用mybatis就是为了使用它半自动orm的特性,通过手动写mapper以防出现不明就里的sql。毕竟和jdbc比起来,不用把取到的数据手动构建为pojo,已经是非常大的便利了。

想进一步省略一些基本的CRUD,只写个性化的sql?mybatis-plus!

xml mapper

先不考虑Java类,单纯使用xml表示mapper。

比如查询:

1
2
3
4
5
<mapper namespace="com.puppylpg.Person">
    <select id="selectPerson" parameterType="int" resultType="hashmap">
      SELECT * FROM PERSON WHERE ID = #{id}
    </select>
</mapper>

语句里比较重要的属性有:

  • id:在该命名空间中唯一的标识符(statement id),可以通过它来引用这条语句
  • resultType:返回结果的类型,可以是
  • resultMap:一个自定义的orm映射规则,见下文。和resultType二选一;

不同的语句还有不同的属性,比如insert和update有:

  • useGeneratedKeys:会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键
1
2
3
4
5
<insert id="insertAuthor" useGeneratedKeys="true"
    keyProperty="id">
  insert into Author (username,password,email,bio)
  values (#{username},#{password},#{email},#{bio})
</insert>

属性有很多,需要的时候再具体查:

  • https://mybatis.org/mybatis-3/zh/sqlmap-xml.html

type handler:类型映射

无论使用resultType还是显式写resultMap,其中的每一个数据类型都需要做转换:数据库的jdbcType和java的javaType互转。转换工具就是typeHandlers

对于基本数据类型和一些其他的常见类型(比如JSR-310时间日期类型),mybatis都内置了一些typeHandlers。比如:

  • BooleanTypeHandler:负责java.lang.Boolean/boolean和jdbc的BOOLEAN互转;
  • StringTypeHandler:负责java.lang.String和jdbc的CHAR/VARCHAR互转;
  • 等等。

也可以自定义一些type handler,比如自定义java.lang.String和jdbc VARCHAR互转:

1
2
3
4
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {
...
}
  1. mybatis可以从泛型知道该handler处理的java类型。也可以使用@MappedTypes显式声明;
  2. @MappedJdbcTypes声明了jdbc的类型;

这样handler能互转的类型就确定了。

什么时候使用type handler?只有两个场景

  1. input:把java类型转成sql里的jdbc类型。在mybatis生成sql语句的属性(#{property})里;
  2. output:把数据库查询结果转java。在resultMap里;

都和resultMap有关。

resultMap:结果映射

一般使用resultType指定要转成的目标类就能自动满足映射需求了。

除了最基础的将结果映射为map:

1
2
3
4
5
<select id="selectUsers" resultType="map">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

还可以通过指定pojo,直接将结果映射为一个类:

1
2
3
4
5
<select id="selectUsers" resultType="com.someapp.model.User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

或者使用typeAlias给自己的pojo定义一个别名以简化引用:

1
2
3
4
5
6
7
8
9
<!-- mybatis-config.xml 中 -->
<typeAlias type="com.someapp.model.User" alias="User"/>

<!-- SQL 映射 XML 中 -->
<select id="selectUsers" resultType="User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

实际上在使用resultType的时候,mybatis会在背后自动创建一个resultMap,映射的规则是:

  1. 按照sql返回的列名(column)找pojo里的属性(property),忽略大小写
  2. 如果设置了mapUnderscoreToCamelCase=true,会将列名的下划线映射为java的驼峰命名

所以正常情况下都是能通过自动映射(autoMappingBehavior)来搞定结果映射的。

The resultMap element is the most important and powerful element in MyBatis.

如果column和property名称差异很大,自动映射搞不定,也有多种解决方式:

  1. 用sql本身支持的select a as b方式直接返回想要的列名
  2. 手动定义resultMap,做手动映射

还可以把两种方式结合起来,比如下面的例子:手动映射hashed_password为password,再通过sql直接把返回列名改成id和userName以使用自动映射:

1
2
3
4
5
6
7
8
9
10
11
12
<resultMap id="userResultMap" type="User">
  <result property="password" column="hashed_password"/>
</resultMap>

<select id="selectUsers" resultMap="userResultMap">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password
  from some_table
  where id = #{id}
</select>

因此即使需要手写resultMap,也不必把所有需要转换的column to field都写上,不写的那些会做默认映射。

联表查询的映射

如果只是做一个表和一个pojo之间的映射,还是挺简单的。但是如果涉及到联表查询,就可能会在查询结果里产生相同的column,自动映射就不太灵便了。所以mybatis默认自动映射的规则是PARTIAL

  • PARTIAL - 只对非连接属性进行映射。内部定义的嵌套结果映射(也就是连接的属性)涉及到的属性不进行映射。mybatis直接把决定权交给了你自己,必须手动映射。再搞错就只能怪你自己了:D
  • FULL - 自动映射所有属性。但是不建议,真有可能把表A.id映射到表示表B的id的property上了;
  • NONE - 禁用自动映射。仅对手动映射的属性进行映射。

但是全局设置为NONE也太离谱了,连简单映射也要自己手写了!诚如是,mybatis相较于jdbc的优势又被削弱了。所以一般还是设置为PARTIAL如果某种情况真需要禁用自动映射,也可以在某个单独的映射规则上使用autoMapping覆盖全局的autoMappingBehavior

1
2
3
<resultMap id="userResultMap" type="User" autoMapping="false">
  <result property="password" column="hashed_password"/>
</resultMap>

选择type handler

resultMap通过名称把数据库列和对象属性对应上了,接下来就是要选择合适的type handler做二者之间的数据转换了。

resultMap里,java类型是已知的(通过resultType,可以读到目标java类声明,取的类型信息),jdbc类型是未知的(执行sql前就要确定handler,此时jdbc类型是未知的,因为mybatis不会去读数据表的metadata来发现类型,没这功能),如果我们不手动指定jdbcType,相当于mybatis要通过javaType=[TheJavaType], jdbcType=null去选择一个type handler。从3.4.0开始,如果根据javaType只能找到一个handler,mybatis就会直接用它。

在此之前,只有显式设置了@MappedJdbcTypes(includeNullJdbcType=true)才行,表明该handler允许jdbcType未知(null)。

对于java转sql,参数的完整声明形式如下:

1
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
  • javaType:可以推测出来,所以一般是可省略的;
  • jdbcType:可指定也可省略,因为一般javaType就把typeHandler确定下来了;
  • typeHandler:同上,一般也可省略;

比如:

1
2
3
4
<insert id="insertUser" parameterType="User">
  insert into users (id, username, password)
  values (#{id}, #{username}, #{password})
</insert>

这种传参方式非常方便!sql里的属性直接和方法参数User映射起来了!

另一处和这个类似,在定义resultMap的时候,<id><result> tag的property也可以设置这几个参数。比如:

1
2
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>

复杂的resultMap

https://mybatis.org/mybatis-3/sqlmap-xml.html#advanced-result-maps

看得似懂非懂,主要是使用场景还不是很明晰。

整体来看:

  • <id><result>是做单个column的映射,直接注入数据;
  • <constructor>是通过类的构造器注入数据;
  • <association>是“has a”,是1对1的关联关系,比如blog有一个author;
  • <collection>是“has many”,是1对n的关联关系,比如blog有很多评论;

最终大概是把数据映射到这样一个Blog类上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Blog {
    public Blog(int blogId) {
        ...
    }
    
    // <id>
    private int blogId;
    // <result>
    private String title;
    
    // <association>
    public Author author;
    
    // <collection>
    public List<Comment> comments;
    
    // ...
}

但是用mybatis搞这种一对一、一对多的关联,多少显得有点儿复杂了。这是hibernate的强项。

使用

创建好SqlSessionFactory,mapper也映射好了,接下来就是使用mapper操作数据库了。

mybatis主要的java接口是SqlSession。开启session,获取之前配置到SqlSessionFactory里的mapper,直接用就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        // 使用factory创建session
        try (SqlSession session = sqlSessionFactory.openSession()) {

            // 从session获取mapper
            BlogMapper mapper = session.getMapper(BlogMapper.class);

            // 通过mapper操作
            Blog elasticsearch = new Blog(1, "elasticsearch");
            Blog mybatis = new Blog(2, "mybatis");

            int result = mapper.insert(elasticsearch);
            System.out.println("insert es: " + result);
            int result2 = mapper.insert(mybatis);
            System.out.println("insert mybatis: " + result2);

            List<Blog> blogs = mapper.getAll();
            blogs.forEach(System.out::println);
        }

如果使用xml定义的mapper,需要在调用的时候传入xml里sql语句的id,aka statement id

1
int insert(String statement, Object parameter)

spring的JdbcTemplate接收的是sql语句,因为没有mapper。mybatis定义了mapper,所以这里传的是mapper的statement的id

比如:

1
2
3
    <select id="selectPerson" parameterType="int" resultType="hashmap">
    SELECT * FROM PERSON WHERE ID = #{id}
    </select>

用的时候就是SqlSession#selectOne("selectPerson", 5)

但是传入statement id有点儿过于繁琐了,而且它们并不符合类型安全,对IDE和单元测试也不是那么友好。所以mapper class出现了!

方法的返回值(insert、update 以及 delete)表示受该语句影响的行数。

mapper class

mapper class出现的作用就是“标识对应的映射语句”。使用的时候直接调用mapper class的方法就行。

mapper class的method名也是statement id。

所以mybatis官方表示mapper class可以做个纯傀儡:

映射器接口不需要去实现任何接口或继承自任何类。只要方法签名可以被用来唯一识别对应的映射语句就可以了。

不过mybatis3加入了基于Java注解的sql配置方式,所以也可以不写xml,只通过mapper class做sql操作。

比如:

1
2
3
@Insert("insert into table3 (id, name) values(#{nameId}, #{name})")
@SelectKey(statement="call next value for TestSequence", keyProperty="nameId", before=true, resultType=int.class)
int insertTable3(Name name);

但是Java注解的灵活性有限,所以一些复杂的表达可能还是离不开xml。

不幸的是,Java 注解的表达能力和灵活性十分有限。尽管我们花了很多时间在调查、设计和试验上,但最强大的 MyBatis 映射并不能用注解来构建——我们真没开玩笑。而 C# 属性就没有这些限制,因此 MyBatis.NET 的配置会比 XML 有更大的选择余地。虽说如此,基于 Java 注解的配置还是有它的好处的。

因此,mapper class和xml mapper同时存在也是常事。

相关代码示例

其他

mybatis还有一些其他有用的高级特性。

intercepter

https://mybatis.org/mybatis-3/zh/configuration.html#%E6%8F%92%E4%BB%B6%EF%BC%88plugins%EF%BC%89

sql generator

帮助生成sql:

  • https://mybatis.org/mybatis-3/zh/statement-builders.html

mybatis-spring

为什么有mybatis-spring?因为spring3发布的时候mybatis3(还叫ibatis3)还没发布,没搭上spring3的车。想让spring对mybatis提供原生支持得等到下一个大版本spring4,等的时间又太久,所以不如自己搞个mybatis-spring把mybatis3接入spring。

bean

使用mybatis-spring,主要是方便地将mybatis最重要的两个东西配置为spring的bean:

  • SqlSessionFactory
  • mapper;

配置SqlSessionFactory用的是SqlSessionFactoryBean,它是一个FactoryBean(本来应该叫SqlSessionFactory_FactoryBean的:D):

1
2
3
4
5
6
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        return factoryBean.getObject();
    }

通过SqlSessionFactoryBean可以控制SqlSessionFactory里一些常用的属性,比如:

  • configLocation以xml的方式配置mybatis
  • mapperLocations配置xml mapper的位置

xml mapper应该从哪里加载?根据对plain mybatis的理解:

  • xml mapper是通过<mapper>注解配置到SqlSessionFactory的;
  • 以Java mapper class配置mybatis时,由于调用了Configuration#addMapper,通过mapper class顺藤摸瓜关联到xml mapper;

所以:

  1. 如果xml mapper的位置和mapper class的位置一致,就不用操心了;
  2. 如果不一致,可以:
    1. 通过configLocation配置mybatis xml的位置,然后在mybatis xml里配置好<mapper>
    2. 通过mapperLocations配置xml mapper的位置:SqlSessionFactoryBean#setMapperLocations(Resource... mapperLocations)

mapperLocations: Set locations of MyBatis mapper files that are going to be merged into the SqlSessionFactory configuration at runtime. This is an alternative to specifying “" entries in an MyBatis config file. This property being based on Spring's resource abstraction also allows for specifying resource patterns here: e.g. "classpath*:sqlmap/*-mapper.xml".

这种情况下xml mapper的结构可以和mapper class无关。但最好对应,方便维护。

The value can contain Ant-style patterns to load all files in a directory or to recursively search all paths from a base location.

因此配置SqlSessionFactory时可能要显式设置xml mapper的位置:

1
2
3
4
5
6
7
8
9
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        
        // 显式设置xml mapper的位置,如果和mapper class位置结构一致,也可以不显式设置
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_XML_PATH));
        return factoryBean.getObject();
    }

同理构建mapper bean也有一个FactoryBeanMapperFactoryBean。只要把SqlSessionFactory和mapper interface设置进去就行:

1
2
3
4
5
6
    @Bean
    public MapperFactoryBean<UserMapper> userMapper() throws Exception {
        MapperFactoryBean<UserMapper> factoryBean = new MapperFactoryBean<>(UserMapper.class);
        factoryBean.setSqlSessionFactory(sqlSessionFactory());
        return factoryBean;
    }

MapperFactoryBean在注册mapper bean的时候,调用了Configuration#addMapper,所以就“顺藤摸瓜”了,相关的xml mapper也注册进来了。

之后就可以使用@Autowire直接注入mapper bean使用了,不必像plain mybatis一样先从SqlSessionFactory获取mapper:

1
2
// 从session获取mapper
BlogMapper mapper = session.getMapper(BlogMapper.class);

方便了许多。

@MapperScan

但是使用MapperFactoryBean把mapper一个一个配置为bean,还是有些不方便的。所以mybatis-spring还提供了@MapperScan注解。自动扫描mapper,生成mapper bean。

但是注意:@MapperScan扫描的并不是@Mapper注解!扫描什么注解是自定义的,并没有和@Mapper绑定。

@MapperScan可以传很多参数:

  • String[] basePackages() default {}:Base packages to scan for MyBatis interfaces. Note that only interfaces with at least one method will be registered; concrete classes will be ignored.(If specific packages are not defined, scanning will occur from the package of the class that declares this annotation.)
  • Class<?>[] basePackageClasses():Type-safe alternative to basePackages() for specifying the packages to scan for annotated components. The package of each class specified will be scanned. Consider creating a special no-op marker class or interface in each package that serves no purpose other than being referenced by this attribute.
  • Class<? extends Annotation> annotationClass():This property specifies the annotation that the scanner will search for. The scanner will register all interfaces in the base package that also have the specified annotation. Note this can be combined with markerInterface.
  • Class<?> markerInterface() default Class.class:This property specifies the parent that the scanner will search for. The scanner will register all interfaces in the base package that also have the specified interface class as a parent. Note this can be combined with annotationClass.

这几个属性才是决定了扫描哪些interface生成mapper bean!跟@Mapper注解完全没关系。

处理@MapperScan注解的类是MapperScannerRegistrar@MapperScan配置的上述属性都被放进了MapperScannerConfigurer,后者实际使用ClassPathMapperScanner在classpath上扫描上述通过@MapperScan接口配置的接口。

MapperScannerRegistrar也可以看到,它会把符合上述配置条件的、至少有一个方法的接口扫描为mapper bean,非接口会被忽略

BeanDefinitionRegistryPostProcessor that searches recursively starting from a base package for interfaces and registers them as MapperFactoryBean. Note that only interfaces with at least one method will be registered; concrete classes will be ignored.

@MapperScan有一个比较不起眼的属性:

  • Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class:Specifies a custom MapperFactoryBean to return a mybatis proxy as spring bean.

从中可以推断所有scan出来的mapper class,都会通过MapperFactoryBean构建为mapper bean。而mapper bean默认调用了Configuration#addMapper,“顺藤摸瓜”找到xml mapper了。所以所有@MapperScan的bean一定也都自动注册xml mapper(如果有的话)

如果xml mapper和他们的位置结构不对应,通过SqlSessionFactoryBean#setMapperLocations(Resource... mapperLocations)就很方便地解决问题了。

最后,@MapperScan还可以设置两个比较有用的属性:

  • Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class
  • String sqlSessionFactoryRef() default "":Specifies which SqlSessionFactory to use in the case that there is more than one in the spring context. Usually this is only needed when you have more than one datasource.

xml mapper的编译问题

不管xml mapper的位置结构是否和mapper class一致,都有办法注册到mybatis里,前提是xml mapper必须得在classpath上。

如果xml mapper非得放到src/main/java里(比如/src/main/java/mapper/xml/下):

1
2
3
4
5
6
7
8
9
└─src
    └─main
        ├─java
        │  ├─config
        │  ├─entity
        │  └─mapper
        │      └─xml
        └─resources
            └─scripts

maven编译的时候是不会理会这些非java文件的!结果就是编译后的classpath上没有这些xml mapper……所以最好把xml mapper挪到resources目录下。

如果还是不想挪,那就显式配置maven把xml也放到classpath下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    <build>
        <resources>
            <!-- 引入mapper对应的xml文件 -->
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
    </build>

另外如果配置了这个显式的resource,默认的src/main/resources就失效了……所以它俩都得写上……

所以@Mapper有什么用?

在mybatis里确没什么卵用:https://github.com/mybatis/spring-boot-starter/issues/46

I have recoded this to be more aligned with Spring @configuration that is based on annotations. So I have added a @Mapper marker annotation to the core so other DI frameworks can use it (spring, boot, cdi)

它就是个marker,mybatis和mybatis-spring里没用它,但是mybatis-spring-boot-starter里用它了!

SqlSessionTemplate

TBD:https://mybatis.org/spring/zh/sqlsession.html#SqlSessionTemplate

使用

mybatis的使用方式是获取mapper bean,使用mapper bean做CRUD。既然这里是spring,肯定以spring的方式获取mapper bean,然后直接使用就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    public static void main(String... args) {

        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();

        applicationContext.register(MybatisConfig.class);
        applicationContext.refresh();

        // 从spring容器获取mapper
        BlogMapper mapper = applicationContext.getBean(BlogMapper.class);

        // 通过mapper操作
        Blog elasticsearch = new Blog(1, "elasticsearch");
        Blog mybatis = new Blog(2, "mybatis");

        int result = mapper.insert(elasticsearch);
        System.out.println("insert es: " + result);
        int result2 = mapper.insert(mybatis);
        System.out.println("insert mybatis: " + result2);

        List<Blog> blogs = mapper.getAll();
        blogs.forEach(System.out::println);

        System.out.println("find by xml---");
        List<Blog> blogsByXml = mapper.getAllByXml();
        blogsByXml.forEach(System.out::println);

        applicationContext.close();
    }

spring的具体的配置就是上面说的SqlSessionFactory@MapperScan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Configuration
@MapperScan(basePackages = MybatisConfig.MAPPER_PACKAGE)
public class MybatisConfig {

    public static final String MAPPER_PACKAGE = "mapper";

    /**
     * Java不编译xml,所以target里没有这些文件。需要maven里设置resources,include xml文件才行。
     * 而且必须使用maven compile才行。所以这是一个纯maven的trick,和java无关
     */
    public static final String MAPPER_XML_PATH = "classpath:mapper/xml/*.xml";

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        // 显式设置xml mapper的位置,如果和mapper class位置结构一致,也可以不显式设置
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_XML_PATH));
        return factoryBean.getObject();
    }

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        // 启动h2时初始化sql脚本
        config.setJdbcUrl("jdbc:h2:mem:pokemon;DB_CLOSE_DELAY=-1;MODE=MySQL;INIT=RUNSCRIPT FROM 'classpath:scripts/init.sql'");
        config.setUsername("sa");
        config.setPassword("password");
        config.setDriverClassName("org.h2.Driver");
        return new HikariDataSource(config);
    }
}

相关代码示例

mybatis-spring-boot-starter

又到了springboot autoconfig了。现在再看autoconfig就老乡见老乡了:D

如果用了mybatis-spring-boot-autoconfig,使用mybatis时只需要干一件事就行了:写一个mapper class并标记上@Mapper注解(也许还要再来个xml mapper)

1
2
3
4
5
6
7
@Mapper
public interface CityMapper {

  @Select("SELECT * FROM CITY WHERE state = #{state}")
  City findByState(@Param("state") String state);

}

别的都不用管了!

自动配置了哪些东西?看一下MybatisAutoConfiguration便一目了然。它使用DataSource配置了:

  • SqlSessionFactory
  • SqlSessionTemplate

此外,在没有任何MapperFactoryBeanMapperScannerConfigurer的情况下,自动配置了一个MapperScannerConfigurer,扫描mapper class生成MapperFactoryBean

扫描的路径是啥?MybatisAutoConfiguration里设置了basePackage

1
2
      builder.addPropertyValue("annotationClass", Mapper.class);
      builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));

如果开debug,会发现用的package:Using auto-configuration base package ‘<base-package>‘,其实就是spring boot默认扫描的package(标注@SpringBootApplication的package)

它里面还有这么一段注释:

This will just scan the same base package as Spring Boot does.

说明和springboot默认扫描的包一致。

自动配置出来的MapperScannerConfigurer还设置了一个annotationClass,值为@Mapper。所以只有springboot工程自动扫描的包里标记了@Mapper的接口才会被注册为mapper bean!

相当于@MapperScan(basePackages = <base package>, annotationClass = Mapper.class)@Mapper千呼万唤始出来……

而根据@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })可知:

  • 手动配置一个MapperFactoryBean会阻止这一自动配置行为;
  • 显式使用@MapperScan也可以改变这一策略。因为发现如果使用了@MapperScan,mybatis-spring会抢先构造出一个MapperScannerConfigurer

这样的话@Mapper的自动注册也不生效了。需要自己配置我们的@MapperScan。但是mybatis-spring-boot-autoconfig告诉我们,如果满足需求,尽量别显式设置@MapperScan,这样就能体验到spring data jpa一样的开箱即用的体验:

If you want more power, you can explicitly use {@link org.mybatis.spring.annotation.MapperScan} but this will get typed mappers working correctly, out-of-the-box, similar to using Spring Data JPA repositories.

配置属性

既然是springboot的autoconfig,自然少不了一些可个性化设置的:

  • https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/zh/index.html#%E9%85%8D%E7%BD%AE

还得是springboot啊……

相关代码示例

mybatis-plus

mybatis-plus增强了mybatis的orm功能:

  1. 通过注解自动把数据库column和java类的property关联起来了;
  2. 同时把一些常用的CRUD的功能封装起来放入了BaseMapper,只要我们自己的mapper class继承这个base mapper,就自动拥有了常用的CRUD statement。

mybatis-plus和spring是强绑定的,所以mybatis-plus相当于就是mybatis-plus-spring。配置bean的时候只做一处替换就行了:使用mybatis-plus自己的MybatisSqlSessionFactoryBean(而不是mybatis的SqlSessionFactoryBean)去创建SqlSession,其他完全不变!

所以创建SqlSessionFactory的时候只需要把mybatis的SqlSessionFactoryBean换成MP的MybatisSqlSessionFactoryBean就行了:

1
2
3
4
5
6
7
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        // 唯一区别:使用mybatisplus自己的sql session factory bean
        MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        return factoryBean.getObject();
    }

再和mybatis-spring一样使用@MapperScan扫描注册mapper bean。

之后的使用方式和mybatis-spring一样了:通过spring获取mapper bean,CRUD。

最大的区别是mapper class,继承BaseMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 继承了mp的base mapper,就不用写一大堆基础的方法了。
 *
 * @author puppylpg on 2022/05/31
 */
public interface BlogMapper extends BaseMapper<Blog> {

//    @Select("select * from BLOG")
//    List<Blog> getAll();
//
//    @Insert("insert into BLOG (id, title) values (#{id}, #{title})")
//    int insert(Blog blog);
//
//    @Select("SELECT * FROM BLOG WHERE id = #{blogId}")
//    Blog getUser(@Param("blogId") String blogId);
}

基本的CRUD都不用写了!

相关代码示例

高级特性

还是有很多的,慢慢看吧:

  • https://baomidou.com/pages/779a6e/#%E4%BD%BF%E7%94%A8

TBD

  • 多数据源:https://baomidou.com/pages/a61e1b/#dynamic-datasource
  • 示例工程:https://github.com/baomidou/awesome-mybatis-plus
  • 示例工程:https://github.com/baomidou/mybatis-plus-samples

mybatis-plus-spring-boot-starter

和mybatis-spring-boot-starter一样,使用@Mapper创建一个mapper class就完事儿了,SqlSessionFactory和mapper bean自动就有了。

当然这个bean必须是继承BaseMapper的,才能有那么多CRUD:

1
2
3
4
@Mapper
public interface UserMapper extends BaseMapper<User> {

}

相关代码示例

Test

mybatis-test

mybatis-spring-boot-starter-test

  • https://mybatis.org/spring-boot-starter/mybatis-spring-boot-test-autoconfigure/index.html

mybatis-plus-test

mybatis-plus-boot-starter-test

  • https://baomidou.com/pages/b7dae0/#%E7%A4%BA%E4%BE%8B%E5%B7%A5%E7%A8%8B

奇奇怪怪的知识

mybatis的前身是iBatis,来自abatis(鹿砦、铁丝网),因为2001年它一开始专注的是密码学加密领域。后来ibatis的团队对基于新技术对其进行了升级,需要重命名,发现batis是一种可爱的小鸟,所以就起名MyBatis。

ibatis率先在数据库层创建了两种框架:sql maps和DAO

Shortly after releasing JPetStore, questions and requests for the SQL Maps and DAO frameworks spawned the project that would become known as iBATIS Database Layer. The iBATIS Database Layer includes two frameworks packaged together: SQL Maps and DAO.

感想

多看几个orm框架,发现也都差不多了。所以看一个东西,最重要的是明白它要解决的是什么问题,怎么解决的。而这往往是通过对比多个相似框架才能深刻理解的……甚至有时候经过对比才更加深刻地理解他们要解决的是什么问题……

本文由作者按照 CC BY 4.0 进行授权