六、Example 用法
通用 Mapper 中的 Example 方法有两大类定义,一个参数和两个参数的,例如下面两个:
List<T> selectByExample(Object example);
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
用法如下:
CountryExample example = new CountryExample();
example.createCriteria().andCountrynameLike("A%");
example.or().andIdGreaterThan(100);
example.setDistinct(true);
int count = mapper.deleteByExample(example);
对于的 SQL 日志如下:
DEBUG [main] - ==> Preparing: DELETE FROM country WHERE ( countryname like ? ) or ( Id > ? )
DEBUG [main] - ==> Parameters: A%(String), 100(Integer)
DEBUG [main] - <== Updates: 95
生成的 CountryExample 中包含了和字段相关的多种方法,根据自己的需要设置相应的条件即可。
6.2 通用 Example
这是由通用 Mapper 提供的一个类,这个类和 MBG 生成的相比,需要自己设置属性名。这个类还额外提供了更多的方法。
6.2.1 查询
示例:
Example example = new Example(Country.class);
example.setForUpdate(true);
example.createCriteria().andGreaterThan("id", 100).andLessThan("id",151);
example.or().andLessThan("id", 41);
List<Country> countries = mapper.selectByExample(example);
日志:
DEBUG [main] - ==> Preparing: SELECT id,countryname,countrycode FROM country WHERE ( id > ? and id < ? ) or ( id < ? ) ORDER BY id desc FOR UPDATE
DEBUG [main] - ==> Parameters: 100(Integer), 151(Integer), 41(Integer)
DEBUG [main] - <== Total: 90
6.2.2 动态 SQL
示例:
Example example = new Example(Country.class);
Example.Criteria criteria = example.createCriteria();
if(query.getCountryname() != null){
criteria.andLike("countryname", query.getCountryname() + "%");
}
if(query.getId() != null){
criteria.andGreaterThan("id", query.getId());
}
List<Country> countries = mapper.selectByExample(example);
日志:
DEBUG [main] - ==> Preparing: SELECT id,countryname,countrycode FROM country WHERE ( countryname like ? ) ORDER BY id desc
DEBUG [main] - ==> Parameters: China%(String)
DEBUG [main] - <== Total: 1
6.2.3 排序
示例:
Example example = new Example(Country.class);
example.orderBy("id").desc().orderBy("countryname").orderBy("countrycode").asc();
List<Country> countries = mapper.selectByExample(example);
日志:
DEBUG [main] - ==> Preparing: SELECT id,countryname,countrycode FROM country order by id DESC,countryname,countrycode ASC
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 183
6.2.4 去重
示例:
CountryExample example = new CountryExample();
//设置 distinct
example.setDistinct(true);
example.createCriteria().andCountrynameLike("A%");
example.or().andIdGreaterThan(100);
List<Country> countries = mapper.selectByExample(example);
日志:
DEBUG [main] - ==> Preparing: SELECT distinct id,countryname,countrycode FROM country WHERE ( countryname like ? ) or ( Id > ? ) ORDER BY id desc
DEBUG [main] - ==> Parameters: A%(String), 100(Integer)
DEBUG [main] - <== Total: 95
6.2.5 设置查询列
示例:
Example example = new Example(Country.class);
example.selectProperties("id", "countryname");
List<Country> countries = mapper.selectByExample(example);
日志:
DEBUG [main] - ==> Preparing: SELECT id , countryname FROM country ORDER BY id desc
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 183
除了这里提到的方法外,还有很多其他的方法,可以查看 Example 源码进行了解。
6.3 Example.builder 方式
示例:
Example example = Example.builder(Country.class)
.select("countryname")
.where(Sqls.custom().andGreaterThan("id", 100))
.orderByAsc("countrycode")
.forUpdate()
.build();
List<Country> countries = mapper.selectByExample(example);
日志:
DEBUG [main] - ==> Preparing: SELECT countryname FROM country WHERE ( id > ? ) order by countrycode Asc FOR UPDATE
DEBUG [main] - ==> Parameters: 100(Integer)
DEBUG [main] - <== Total: 83
6.4 Weekend
使用 6.2 和 6.3 中的 Example 时,需要自己输入属性名,例如 "countryname"
,假设输入错误,或者数据库有变化,这里很可能就会出错,因此基于 Java 8 的方法引用是一种更安全的用法,如果你使用 Java 8,你可以试试 Weekend。
示例:
List<Country> selectByWeekendSql = mapper.selectByExample(new Example.Builder(Country.class)
.where(WeekendSqls.<Country>custom().andLike(Country::getCountryname, "%a%")
.andGreaterThan(Country::getCountrycode, "123"))
.build());
日志:
DEBUG [main] - ==> Preparing: SELECT id,countryname,countrycode FROM country WHERE ( countryname like ? and countrycode > ? )
DEBUG [main] - ==> Parameters: %a%(String), 123(String)
DEBUG [main] - <== Total: 151
在代码中的 Country::getCountryname
就是方法引用,通过该方法可以自动转换对应的列名。
7.1 二级缓存
关于二级缓存的例子,可以查看测试中的 tk.mybatis.mapper.cache.CacheTest
。
首先需要开启二级缓存:
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 其他 -->
</configuration>
7.1.1 只使用接口
只用接口时,只需要加一个缓存的注解,示例如下:
/**
* 只有接口时,加下面的注解即可
*/
@CacheNamespace
public interface CountryCacheMapper extends Mapper<Country> {
}
对缓存的详细配置可以通过该注解提供的属性进行配置。
7.1.2 接口和 XML 混合
由于 MyBatis 目前处理 XML 和 接口中的引用时存在 BUG,所以只有这里提供的一种方式进行配置。也就是在 XML 中配置
<cache/>
,在接口中使用@CacheNamespaceRef(CountryCacheRefMapper.class)
引用注解。 关于该 BUG 可以查看下面链接: https://github.com/mybatis/mybatis-3/issues/1194
在 XML 中定义缓存:
<?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="tk.mybatis.mapper.cache.CountryCacheRefMapper">
<cache/>
<select id="selectById" resultType="tk.mybatis.mapper.base.Country">
select * from country where id = #{id}
</select>
</mapper>
在接口中配置注解引用:
@CacheNamespaceRef(CountryCacheRefMapper.class)
//或者 @CacheNamespaceRef(name = "tk.mybatis.mapper.cache.CountryCacheRefMapper")
public interface CountryCacheRefMapper extends Mapper<Country> {
/**
* 定义在 XML 中的方法
*
* @param id
* @return
*/
Country selectById(Integer id);
}
@CacheNamespaceRef
指定的是缓存的 namespace
,就是 XML 中 <mapper>
中的 namespace
属性。
7.1.3 潜在的问题
通用 Mapper 中部分 insert, update 方法使用了 @Options
注解,在 4.0 版本中 update 方法都去掉了这个注解,但是部分 insert 必须保留。
存在
@Options
注解的方法:
tk.mybatis.mapper.common.special.InsertListMapper
int insertList(List<? extends T> recordList);
tk.mybatis.mapper.common.special.InsertUseGeneratedKeysMapper
int insertUseGeneratedKeys(T record);
tk.mybatis.mapper.common.sqlserver.InsertMapper
int insert(T record);
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 以及更旧的版本中:
boolean flushCache() default false;
3.4.0+中:
/**
* The options for the {@link Options#flushCache()}.
* The default is {@link FlushCachePolicy#DEFAULT}
*/
public enum FlushCachePolicy {
/** <code>false</code> for select statement; <code>true</code> for insert/update/delete statement. */
DEFAULT,
/** Flushes cache regardless of the statement type. */
TRUE,
/** Does not flush cache regardless of the statement type. */
FALSE
}
FlushCachePolicy flushCache() default FlushCachePolicy.DEFAULT;
很显然,在 3.4.0+ 中的定义更合理,所以如果使用二级缓存,建议升级到比较新的版本。
7.2 TypeHandler 类型处理器
本文通过两个例子来演示 typeHandler 的用法。
示例来自 base/src/test/java/tk.mybatis.mapper.typehandler 包中的测试。
在开始例子前,先上相同的代码。
一个简单的枚举类:
public enum StateEnum {
disabled,
enabled,
}
这里打算用 MyBatis 提供的 EnumOrdinalTypeHandler
,也是数据库存储枚举对应的序号。
一个简单的地址类:
public class Address implements Serializable {
private static final long serialVersionUID = 1L;
private String province;
private String city;
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
if(province != null && province.length() > 0){
builder.append(province);
}
if(city != null && city.length() > 0){
builder.append("/").append(city);
}
return builder.toString();
}
}
这个类用来演示自定义类型的用法,针对该类写一个自定义的 TypeHandler,如下:
public class AddressTypeHandler extends BaseTypeHandler<Address> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Address parameter,
JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.toString());
}
private Address convertToAddress(String addressStr){
if(addressStr == null || addressStr.length() == 0){
return null;
}
String[] strings = addressStr.split("/");
Address address = new Address();
if(strings.length > 0 && strings[0].length() > 0){
address.setProvince(strings[0]);
}
if(strings.length > 1 && strings[1].length() > 0){
address.setCity(strings[1]);
}
return address;
}
@Override
public Address getNullableResult(ResultSet rs, String columnName) throws SQLException {
return convertToAddress(rs.getString(columnName));
}
@Override
public Address getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return convertToAddress(rs.getString(columnIndex));
}
@Override
public Address getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
return convertToAddress(cs.getString(columnIndex));
}
}
测试数据如下:
create table user (
id integer NOT NULL PRIMARY KEY,
name varchar(32),
address varchar(64),
state integer
);
INSERT INTO user (id, name, address, state) VALUES (1, 'abel533', 'Hebei/Shijiazhuang', 1);
INSERT INTO user (id, name, address, state) VALUES (2, 'isea533', 'Hebei/Handan', 0);
特别注意,state 对应的 0 就是枚举第一个 disabled,1 就是枚举第二个 enabled。
7.2.1 使用 ColumnType 注解指定
创建 user 表对应的 User 实体:
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Integer id;
private String name;
@ColumnType(typeHandler = AddressTypeHandler.class)
private Address address;
private StateEnum state;
//省略 setter 和 getter
}
特别提醒
- 默认情况下只有简单类型才会被当作表中的字段(
useSimpleType=true
)。- 当字段有
@Column
或者@ColumnType
注解时,也会被当作表中的字段。- 默认情况下,枚举不会当作表中的字段,如果想要自动把枚举作为表中字段,需要配置
enumAsSimpleType=true
,这里的例子就启用了这个配置。如果不启用这个功能,也需要加@Column
或者@ColumnType
注解。
在这个例子中,address
字段通过 @ColumnType
设置了 typeHandler
。但是 state
却没这么设置,先看 EnumOrdinalTypeHandler
的定义:
public class EnumOrdinalTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E>
由于需要指定泛型,因此这里不能直接配置,除非你在创建一个针对性的 TypeHandler,例如:
public class StateEnumTypeHandler extends EnumOrdinalTypeHandler<StateEnum> {
public StateEnumTypeHandler(Class<StateEnum> type) {
super(type);
}
}
然后配置:
@ColumnType(typeHandler = StateEnumTypeHandler.class)
private StateEnum state;
除了用这种麻烦的方式外,还可以直接用下面的方式配置全局的 typeHandler:
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 其他配置 -->
<typeHandlers>
<typeHandler
handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
javaType="tk.mybatis.mapper.typehandler.StateEnum"/>
</typeHandlers>
<!-- 其他配置 -->
</configuration>
有了这些配置后,就可以在增删改查使用了。
这个例子对应的测试: tk.mybatis.mapper.typehandler.TypeHandlerTest
。
7.2.2 全局配置
第二种就是全部在配置文件中使用 typeHandler 进行配置,实体只做简单的配置:
@Table(name = "user")
public class User2 implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Integer id;
private String name;
@Column
private Address address;
private StateEnum state;
//省略 setter 和 getter
}
这里的 Address
加上 @Column
注解,只是为了把该字段当成表中的字段。
typeHandler 全局配置如下:
<typeHandlers>
<typeHandler
handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
javaType="tk.mybatis.mapper.typehandler.StateEnum"/>
<typeHandler handler="tk.mybatis.mapper.typehandler.AddressTypeHandler"/>
</typeHandlers>
做好这些配置后,就可以在增删改查使用了。
这个例子对应的测试: tk.mybatis.mapper.typehandler.TypeHandlerTest2
。