六、Example 用法

通用 Mapper 中的 Example 方法有两大类定义,一个参数和两个参数的,例如下面两个:

  1. List<T> selectByExample(Object example);
  2. int updateByExampleSelective(@Param("record") T record, @Param("example") Object example);

所有 Example 方法中的 example 类型都是 Object 类型,这是因为通用 Mapper 支持所有符合 Example 结构的参数,例如通过 MBG 生成的 CountryExample、UserExample 类。还有通用 Mapper 中提供的通用 Example,以及支持 Java8 方法引用的 Weekend 类型。

配置中有一个和 Example 有关的参数,点击查看 3.14 checkExampleEntityClass

6.1 MBG 生成的 Example

用法如下:

  1. CountryExample example = new CountryExample();
  2. example.createCriteria().andCountrynameLike("A%");
  3. example.or().andIdGreaterThan(100);
  4. example.setDistinct(true);
  5. int count = mapper.deleteByExample(example);

对于的 SQL 日志如下:

  1. DEBUG [main] - ==> Preparing: DELETE FROM country WHERE ( countryname like ? ) or ( Id > ? )
  2. DEBUG [main] - ==> Parameters: A%(String), 100(Integer)
  3. DEBUG [main] - <== Updates: 95

生成的 CountryExample 中包含了和字段相关的多种方法,根据自己的需要设置相应的条件即可。

6.2 通用 Example

这是由通用 Mapper 提供的一个类,这个类和 MBG 生成的相比,需要自己设置属性名。这个类还额外提供了更多的方法。

6.2.1 查询

示例:

  1. Example example = new Example(Country.class);
  2. example.setForUpdate(true);
  3. example.createCriteria().andGreaterThan("id", 100).andLessThan("id",151);
  4. example.or().andLessThan("id", 41);
  5. List<Country> countries = mapper.selectByExample(example);

日志:

  1. DEBUG [main] - ==> Preparing: SELECT id,countryname,countrycode FROM country WHERE ( id > ? and id < ? ) or ( id < ? ) ORDER BY id desc FOR UPDATE
  2. DEBUG [main] - ==> Parameters: 100(Integer), 151(Integer), 41(Integer)
  3. DEBUG [main] - <== Total: 90

6.2.2 动态 SQL

示例:

  1. Example example = new Example(Country.class);
  2. Example.Criteria criteria = example.createCriteria();
  3. if(query.getCountryname() != null){
  4. criteria.andLike("countryname", query.getCountryname() + "%");
  5. }
  6. if(query.getId() != null){
  7. criteria.andGreaterThan("id", query.getId());
  8. }
  9. List<Country> countries = mapper.selectByExample(example);

日志:

  1. DEBUG [main] - ==> Preparing: SELECT id,countryname,countrycode FROM country WHERE ( countryname like ? ) ORDER BY id desc
  2. DEBUG [main] - ==> Parameters: China%(String)
  3. DEBUG [main] - <== Total: 1

6.2.3 排序

示例:

  1. Example example = new Example(Country.class);
  2. example.orderBy("id").desc().orderBy("countryname").orderBy("countrycode").asc();
  3. List<Country> countries = mapper.selectByExample(example);

日志:

  1. DEBUG [main] - ==> Preparing: SELECT id,countryname,countrycode FROM country order by id DESC,countryname,countrycode ASC
  2. DEBUG [main] - ==> Parameters:
  3. DEBUG [main] - <== Total: 183

6.2.4 去重

示例:

  1. CountryExample example = new CountryExample();
  2. //设置 distinct
  3. example.setDistinct(true);
  4. example.createCriteria().andCountrynameLike("A%");
  5. example.or().andIdGreaterThan(100);
  6. List<Country> countries = mapper.selectByExample(example);

日志:

  1. DEBUG [main] - ==> Preparing: SELECT distinct id,countryname,countrycode FROM country WHERE ( countryname like ? ) or ( Id > ? ) ORDER BY id desc
  2. DEBUG [main] - ==> Parameters: A%(String), 100(Integer)
  3. DEBUG [main] - <== Total: 95

6.2.5 设置查询列

示例:

  1. Example example = new Example(Country.class);
  2. example.selectProperties("id", "countryname");
  3. List<Country> countries = mapper.selectByExample(example);

日志:

  1. DEBUG [main] - ==> Preparing: SELECT id , countryname FROM country ORDER BY id desc
  2. DEBUG [main] - ==> Parameters:
  3. DEBUG [main] - <== Total: 183

除了这里提到的方法外,还有很多其他的方法,可以查看 Example 源码进行了解。

6.3 Example.builder 方式

示例:

  1. Example example = Example.builder(Country.class)
  2. .select("countryname")
  3. .where(Sqls.custom().andGreaterThan("id", 100))
  4. .orderByAsc("countrycode")
  5. .forUpdate()
  6. .build();
  7. List<Country> countries = mapper.selectByExample(example);

日志:

  1. DEBUG [main] - ==> Preparing: SELECT countryname FROM country WHERE ( id > ? ) order by countrycode Asc FOR UPDATE
  2. DEBUG [main] - ==> Parameters: 100(Integer)
  3. DEBUG [main] - <== Total: 83

6.4 Weekend

使用 6.2 和 6.3 中的 Example 时,需要自己输入属性名,例如 "countryname",假设输入错误,或者数据库有变化,这里很可能就会出错,因此基于 Java 8 的方法引用是一种更安全的用法,如果你使用 Java 8,你可以试试 Weekend。

示例:

  1. List<Country> selectByWeekendSql = mapper.selectByExample(new Example.Builder(Country.class)
  2. .where(WeekendSqls.<Country>custom().andLike(Country::getCountryname, "%a%")
  3. .andGreaterThan(Country::getCountrycode, "123"))
  4. .build());

日志:

  1. DEBUG [main] - ==> Preparing: SELECT id,countryname,countrycode FROM country WHERE ( countryname like ? and countrycode > ? )
  2. DEBUG [main] - ==> Parameters: %a%(String), 123(String)
  3. DEBUG [main] - <== Total: 151

在代码中的 Country::getCountryname 就是方法引用,通过该方法可以自动转换对应的列名。

7.1 二级缓存

关于二级缓存的例子,可以查看测试中的 tk.mybatis.mapper.cache.CacheTest

首先需要开启二级缓存:

  1. <!DOCTYPE configuration
  2. PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  3. "http://mybatis.org/dtd/mybatis-3-config.dtd">
  4. <configuration>
  5. <settings>
  6. <setting name="cacheEnabled" value="true"/>
  7. </settings>
  8. <!-- 其他 -->
  9. </configuration>

7.1.1 只使用接口

只用接口时,只需要加一个缓存的注解,示例如下:

  1. /**
  2. * 只有接口时,加下面的注解即可
  3. */
  4. @CacheNamespace
  5. public interface CountryCacheMapper extends Mapper<Country> {
  6. }

对缓存的详细配置可以通过该注解提供的属性进行配置。

7.1.2 接口和 XML 混合

由于 MyBatis 目前处理 XML 和 接口中的引用时存在 BUG,所以只有这里提供的一种方式进行配置。也就是在 XML 中配置 <cache/>,在接口中使用 @CacheNamespaceRef(CountryCacheRefMapper.class) 引用注解。 关于该 BUG 可以查看下面链接: https://github.com/mybatis/mybatis-3/issues/1194

在 XML 中定义缓存:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper
  3. PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  4. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  5. <mapper namespace="tk.mybatis.mapper.cache.CountryCacheRefMapper">
  6. <cache/>
  7. <select id="selectById" resultType="tk.mybatis.mapper.base.Country">
  8. select * from country where id = #{id}
  9. </select>
  10. </mapper>

在接口中配置注解引用:

  1. @CacheNamespaceRef(CountryCacheRefMapper.class)
  2. //或者 @CacheNamespaceRef(name = "tk.mybatis.mapper.cache.CountryCacheRefMapper")
  3. public interface CountryCacheRefMapper extends Mapper<Country> {
  4. /**
  5. * 定义在 XML 中的方法
  6. *
  7. * @param id
  8. * @return
  9. */
  10. Country selectById(Integer id);
  11. }

@CacheNamespaceRef 指定的是缓存的 namespace,就是 XML 中 <mapper> 中的 namespace 属性。

7.1.3 潜在的问题

通用 Mapper 中部分 insert, update 方法使用了 @Options 注解,在 4.0 版本中 update 方法都去掉了这个注解,但是部分 insert 必须保留。

存在 @Options 注解的方法:

  1. tk.mybatis.mapper.common.special.InsertListMapper
    • int insertList(List<? extends T> recordList);
  2. tk.mybatis.mapper.common.special.InsertUseGeneratedKeysMapper
    • int insertUseGeneratedKeys(T record);
  3. tk.mybatis.mapper.common.sqlserver.InsertMapper
    • int insert(T record);
  4. tk.mybatis.mapper.common.sqlserver.InsertSelectiveMapper
    • int insertSelective(T record);

这 4 个方法都是特殊的方法,不是 Mapper<T> 接口中包含的两个 insert 方法。

MyBatis 中的 @Options 注解在 3.3.x 版本和 3.4.0+ 后的版本中,对 flushCache 方法定义不同,这就导致通用 Mapper 中无法直接配置改属性,在 3.3.x 等低版本中,该属性默认 false,因此执行 insert 后不会清除一二级缓存。在高版本中不存在该问题。

因此如果要使用二级缓存,建议 MyBatis 使用比较新的版本,否则需要考虑使用 insert 后可能查询不到的问题。

Options 注解中的 flushCache 区别如下:

3.3.x 以及更旧的版本中:

  1. boolean flushCache() default false;

3.4.0+中:

  1. /**
  2. * The options for the {@link Options#flushCache()}.
  3. * The default is {@link FlushCachePolicy#DEFAULT}
  4. */
  5. public enum FlushCachePolicy {
  6. /** <code>false</code> for select statement; <code>true</code> for insert/update/delete statement. */
  7. DEFAULT,
  8. /** Flushes cache regardless of the statement type. */
  9. TRUE,
  10. /** Does not flush cache regardless of the statement type. */
  11. FALSE
  12. }
  13. FlushCachePolicy flushCache() default FlushCachePolicy.DEFAULT;

很显然,在 3.4.0+ 中的定义更合理,所以如果使用二级缓存,建议升级到比较新的版本。

7.2 TypeHandler 类型处理器

本文通过两个例子来演示 typeHandler 的用法。

示例来自 base/src/test/java/tk.mybatis.mapper.typehandler 包中的测试。

在开始例子前,先上相同的代码。

一个简单的枚举类:

  1. public enum StateEnum {
  2. disabled,
  3. enabled,
  4. }

这里打算用 MyBatis 提供的 EnumOrdinalTypeHandler,也是数据库存储枚举对应的序号。

一个简单的地址类:

  1. public class Address implements Serializable {
  2. private static final long serialVersionUID = 1L;
  3. private String province;
  4. private String city;
  5. public String getProvince() {
  6. return province;
  7. }
  8. public void setProvince(String province) {
  9. this.province = province;
  10. }
  11. public String getCity() {
  12. return city;
  13. }
  14. public void setCity(String city) {
  15. this.city = city;
  16. }
  17. @Override
  18. public String toString() {
  19. StringBuilder builder = new StringBuilder();
  20. if(province != null && province.length() > 0){
  21. builder.append(province);
  22. }
  23. if(city != null && city.length() > 0){
  24. builder.append("/").append(city);
  25. }
  26. return builder.toString();
  27. }
  28. }

这个类用来演示自定义类型的用法,针对该类写一个自定义的 TypeHandler,如下:

  1. public class AddressTypeHandler extends BaseTypeHandler<Address> {
  2. @Override
  3. public void setNonNullParameter(PreparedStatement ps, int i, Address parameter,
  4. JdbcType jdbcType) throws SQLException {
  5. ps.setString(i, parameter.toString());
  6. }
  7. private Address convertToAddress(String addressStr){
  8. if(addressStr == null || addressStr.length() == 0){
  9. return null;
  10. }
  11. String[] strings = addressStr.split("/");
  12. Address address = new Address();
  13. if(strings.length > 0 && strings[0].length() > 0){
  14. address.setProvince(strings[0]);
  15. }
  16. if(strings.length > 1 && strings[1].length() > 0){
  17. address.setCity(strings[1]);
  18. }
  19. return address;
  20. }
  21. @Override
  22. public Address getNullableResult(ResultSet rs, String columnName) throws SQLException {
  23. return convertToAddress(rs.getString(columnName));
  24. }
  25. @Override
  26. public Address getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
  27. return convertToAddress(rs.getString(columnIndex));
  28. }
  29. @Override
  30. public Address getNullableResult(CallableStatement cs, int columnIndex)
  31. throws SQLException {
  32. return convertToAddress(cs.getString(columnIndex));
  33. }
  34. }

测试数据如下:

  1. create table user (
  2. id integer NOT NULL PRIMARY KEY,
  3. name varchar(32),
  4. address varchar(64),
  5. state integer
  6. );
  7. INSERT INTO user (id, name, address, state) VALUES (1, 'abel533', 'Hebei/Shijiazhuang', 1);
  8. INSERT INTO user (id, name, address, state) VALUES (2, 'isea533', 'Hebei/Handan', 0);

特别注意,state 对应的 0 就是枚举第一个 disabled,1 就是枚举第二个 enabled。

7.2.1 使用 ColumnType 注解指定

创建 user 表对应的 User 实体:

  1. public class User implements Serializable {
  2. private static final long serialVersionUID = 1L;
  3. @Id
  4. private Integer id;
  5. private String name;
  6. @ColumnType(typeHandler = AddressTypeHandler.class)
  7. private Address address;
  8. private StateEnum state;
  9. //省略 setter 和 getter
  10. }

特别提醒

  1. 默认情况下只有简单类型才会被当作表中的字段(useSimpleType=true)。
  2. 当字段有 @Column 或者 @ColumnType 注解时,也会被当作表中的字段。
  3. 默认情况下,枚举不会当作表中的字段,如果想要自动把枚举作为表中字段,需要配置 enumAsSimpleType=true这里的例子就启用了这个配置。如果不启用这个功能,也需要加 @Column 或者 @ColumnType 注解。

在这个例子中,address 字段通过 @ColumnType 设置了 typeHandler。但是 state 却没这么设置,先看 EnumOrdinalTypeHandler 的定义:

  1. public class EnumOrdinalTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E>

由于需要指定泛型,因此这里不能直接配置,除非你在创建一个针对性的 TypeHandler,例如:

  1. public class StateEnumTypeHandler extends EnumOrdinalTypeHandler<StateEnum> {
  2. public StateEnumTypeHandler(Class<StateEnum> type) {
  3. super(type);
  4. }
  5. }

然后配置:

  1. @ColumnType(typeHandler = StateEnumTypeHandler.class)
  2. private StateEnum state;

除了用这种麻烦的方式外,还可以直接用下面的方式配置全局的 typeHandler:

  1. <!DOCTYPE configuration
  2. PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  3. "http://mybatis.org/dtd/mybatis-3-config.dtd">
  4. <configuration>
  5. <!-- 其他配置 -->
  6. <typeHandlers>
  7. <typeHandler
  8. handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
  9. javaType="tk.mybatis.mapper.typehandler.StateEnum"/>
  10. </typeHandlers>
  11. <!-- 其他配置 -->
  12. </configuration>

有了这些配置后,就可以在增删改查使用了。

这个例子对应的测试: tk.mybatis.mapper.typehandler.TypeHandlerTest

7.2.2 全局配置

第二种就是全部在配置文件中使用 typeHandler 进行配置,实体只做简单的配置:

  1. @Table(name = "user")
  2. public class User2 implements Serializable {
  3. private static final long serialVersionUID = 1L;
  4. @Id
  5. private Integer id;
  6. private String name;
  7. @Column
  8. private Address address;
  9. private StateEnum state;
  10. //省略 setter 和 getter
  11. }

这里的 Address 加上 @Column 注解,只是为了把该字段当成表中的字段。

typeHandler 全局配置如下:

  1. <typeHandlers>
  2. <typeHandler
  3. handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
  4. javaType="tk.mybatis.mapper.typehandler.StateEnum"/>
  5. <typeHandler handler="tk.mybatis.mapper.typehandler.AddressTypeHandler"/>
  6. </typeHandlers>

做好这些配置后,就可以在增删改查使用了。

这个例子对应的测试: tk.mybatis.mapper.typehandler.TypeHandlerTest2