通用mapper使用指南
一、概述
通用 Mapper是一个可以实现任意 MyBatis 通用方法的框架,项目提供了常规的增删改查操作以及查询相关的单表操作。通用 Mapper 是为了解决 MyBatis 使用中 90% 的基本操作,使用它可以很方便的进行开发,可以节省开发人员大量的时间。
二、集成
Spring集成
第一步:添加依赖
第二步:开始集成
XML配置有以下两种方式,选其一
使用MapperScannerConfigurer
tk.mybatis.spring.mapper.MapperScannerConfigurer代替org.mybatis.spring.mapper.MapperScannerConfigurer
...
使用Configuration
如果某些第三方也需要特殊的 MapperScannerConfigurer 时,就不能用上面的方式进行配置了,此时使用以下方法
notEmpty=true
这里使用了 tk.mybatis.mapper.session.Configuration ,就是不能通过读取 mybatis-config.xml进行配置,直接使用 Spring setter 配置属性
使用java注解和代码集成方式有以下三种
使用@MapperScan的properties属性
@Configuration
@MapperScan(value = "tk.mybatis.mapper.annotation",
properties = {
"mappers=tk.mybatis.mapper.common.Mapper",
"notEmpty=true"
}
)
public class MyBatisConfigProperties {
}
使用@MapperScan的mapperHelperRef 属性
@Configuration
@MapperScan(value = "tk.mybatis.mapper.annotation", mapperHelperRef = "mapperHelper")
public static class MyBatisConfigRef {
//其他
@Bean
public MapperHelper mapperHelper() {
Config config = new Config();
List
mappers.add(Mapper.class);
config.setMappers(mappers);
MapperHelper mapperHelper = new MapperHelper();
mapperHelper.setConfig(config);
return mapperHelper;
}
}
使用构造Configuration的方式
@Configuration
@MapperScan(value = "tk.mybatis.mapper.annotation")
public static class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource());
//tk.mybatis.mapper.session.Configuration
Configuration configuration = new Configuration();
//可以对 MapperHelper 进行配置后 set
configuration.setMapperHelper(new MapperHelper());
//设置为 tk 提供的 Configuration
sessionFactory.setConfiguration(configuration);
return sessionFactory.getObject();
}
}
spring boot集成
基于 starter 的自动配置
在 starter 的逻辑中,如果你没有使用 @MapperScan 注解,你就需要在你的接口上增加 @Mapper注解,否则 MyBatis 无法判断扫描哪些接口。所以需要手动在所有接口上增加 @Mapper 注解
在yml中配置属性
mapper:
mappers:
- tk.mybatis.mapper.common.Mapper
notEmpty: true
基于MapperScan注解
@tk.mybatis.spring.annotation.MapperScan(basePackages = "扫描包")
@SpringBootApplication
public class SampleMapperApplication implements CommandLineRunner {
}
三、注解详解
@NameStyle注解(Mapper)
可以设置实体类字段与表字段的转换方式,可选值
public enum Style {
normal, //原值
camelhump, //驼峰转下划线,默认值
uppercase, //转换为大写
lowercase, //转换为小写
camelhumpAndUppercase, //驼峰转下划线大写形式
camelhumpAndLowercase, //驼峰转下划线小写形式
}
@Table注解(JPA)
作用:建立实体类和数据库表之间的对应关系。
默认规则: 实体类类名首字母小写作为表名。 Employee 类→employee 表
用法: 在@Table注解的 name属性中指定目标数据库表的表名
@Column注解(JPA)
作用:建立实体类字段和数据库表字段之间的对应关系。
默认规则:实体类字段:驼峰式命名。数据库表字段:使用“_”区分各个单词
用法:在@Column 注解的name 属性中指定目标字段的字段名
@ColumnType注解(Mapper)
主要用于枚举属性,其中column属性和 @Column 中的 name 作用相同,但是 @Column的优先级更高。除了 name 属性外,这个注解主要提供了 jdbcType 属性和 typeHandler 属性。
jdbcType 用于设置特殊数据库类型时指定数据库中的 jdbcType。
typeHandler 用于设置特殊类型处理器,常见的是枚举。
@Transient注解(JPA)
用于标记不与数据库表字段对应的实体类字段。对于类中的复杂对象,以及 Map,List 等属性不需要配置这个注解。
主键相关注解
主键策略
自增类型的
使用@Id配合@KeySql,偏向Mybatis写法
@Id
@KeySql(useGeneratedKeys = true)//或者@GeneratedValue(generator = "JDBC")
private Integer id;
使用@Id配合@KeySql,偏向JPA写法
@Id
@KeySql(dialect = IdentityDialect.DEFAULT)//或者直接指定数据库方言dialect = IdentityDialect.MYSQL
private Integer id;
使用@GeneratedValue
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
通过序列和任意 SQL 获取主键值
使用@Id配合@KeySql
@Id
@KeySql(sql = "select SEQ_ID.nextval from dual", order = ORDER.BEFORE)
private Integer id;
使用@GeneratedValue
@Id
@GeneratedValue( strategy = GenerationType.SEQUENCE, generator = "select SEQ_ID.nextval from dual")
private Integer id;
@Verson注解(Mapper)
@Verson注解已经不常用,以下只是简单说明:
在使用乐观锁时,由于通用 Mapper 是内置的实现,不是通过拦截器方式实现的,如果版本不一致可能执行影响数为0,但不会抛出异常,所以在 Java6,7中使用时,你需要自己在调用方法后进行判断是否执行成功。
//Java8+可以使用默认方法判断自动抛出异常
public interface MyMapper
default int deleteWithVersion(T t){
int result = delete(t);
if(result == 0){
throw new RuntimeException("删除失败!");
}
return result;
}
default int updateByPrimaryKeyWithVersion(Object t){
int result = updateByPrimaryKey(t);
if(result == 0){
throw new RuntimeException("更新失败!");
}
return result;
}
}
@RegisterMapper 注解
作用: 通用 Mapper 检测到该接口被继承时,会自动注册。否则需要配置扫描参数。
四、全局主键
第一步:实现GenId
//用UUID实现主键生成策略
public class UUIDGenId implements GenId
@Override
public String genId(String table, String column) {
return UUID.randomUUID().toString().replace("-","");
}
}
第二步:配置
public class User {
@Id
@KeySql(genId = UUIDGenId.class)
private String id;
private String name;
private String code;
public User() {
}
public User(String name, String code) {
this.name = name;
this.code = code;
}
//省略 setter 和 getter
}
如果你使用了@KeySql提供的其他方式,genId就不会生效,genId 是所有方式中优先级最低的
五、常用配置
指配置通用mapper的位置,以xml方式为例
mappers
4.0 之后,增加了一个 @RegisterMapper 注解,通用 Mapper 中提供的所有接口都有这个注解,有了该注解后,通用 Mapper 会自动解析所有的接口,如果父接口(递归向上找到的最顶层)存在标记该注解的接口,就会自动注册上。因此 4.0 后使用通用 Mapper 提供的方法时,不需要在配置这个参数。当自己扩展通用接口时,建议加上该注解,否则就要配置 mappers 参数。
IDENTITY
取回主键的方式,可以配置的值为所列的数据库类型,例如MYSQL: SELECT LAST_INSERT_ID() ,配置为IDENTITY=MYSQL
ORDER
用于配置何时获取主键
catalog、schema
数据库的catalog,如果设置该值,查询的时候表名会带catalog设置的前缀
schema同catalog,catalog优先级高于schema
enumAsSimpleType
用于配置是否将枚举类型当成基本类型对待。默认 simpleType 会忽略枚举类型,使用 enumAsSimpleType 配置后会把枚举按简单类型处理,需要自己配置好 typeHandler。
配置方式如下:enumAsSimpleType=true
checkExampleEntityClass
用于校验通用 Example 构造参数 entityClass 是否和当前调用的 Mapper
safeDelete、safeUpdate
配置为 true 后,delete 和 deleteByExample 都必须设置查询条件才能删除,否则会抛出异常
配置为 true 后,updateByExample 和 updateByExampleSelective 都必须设置查询条件才能更新,否则会抛出异常
六、代码生成器
第一步:引入MBG依赖
第二步:配置generatorConfig.xml
connectionURL="jdbc:mysql://192.168.10.191:3306/test" userId="root" password="123456">
第三步:以java代码的方式运行MBG
public class MybatisGenerator{
public static void main(String[] args) throws IOException, XMLParserException, InvalidConfigurationException, SQLException, InterruptedException {
InputStream configFile = MybatisGenerator.class.getResourceAsStream("/generatorConfig.xml");
System.out.println(configFile);
List
boolean overwrite = true;
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
ProgressCallback progress = new VerboseProgressCallback();
myBatisGenerator.generate(progress);
}
}
七、扩展通用接口
以自定义的SelectAll为例,演示创建通用接口步骤。
第一步:创建接口,接口使用注解@RegisterMapper使通用mapper扫描,使用注解@SelectProvider指定驱动类,method = "dynamicSQL"为固定写法。
@RegisterMapper
public interface SelectAllMapper
// 查询全部结果
@SelectProvider(type = MySelectProvider.class, method = "dynamicSQL")
List
}
第二步:实现驱动类,注意驱动类继承MapperTemplate,创建的方法名需要与接口的方法名同名。
public class MySelectProvider extends MapperTemplate {
public BaseSelectProvider(Class> mapperClass, MapperHelper mapperHelper) {
super(mapperClass, mapperHelper);
}
// 查询全部结果
public String selectAll(MappedStatement ms) {
final Class> entityClass = getEntityClass(ms);
//修改返回值类型为实体类型
setResultType(ms, entityClass);
StringBuilder sql = new StringBuilder();
sql.append(SqlHelper.selectAllColumns(entityClass));
sql.append(SqlHelper.fromTable(entityClass, tableName(entityClass)));
sql.append(SqlHelper.orderByDefault(entityClass));
return sql.toString();
}
}
注:可映射驱动注解类型
@SelectProvider —— 查询驱动类型,返回值为其查询结果集合
@UpdateProvider —— 更新驱动类型,返回值为影响记录行数
@InsertProvider —— 插入驱动类型,返回值为影响记录行数,主键填充在传入的参数中
@DeleteProvider —— 删除驱动类型,返回值为影响记录行数
注:对开发自定义有帮助的类
MapperTemplate —— 通用Mapper模板类,扩展通用Mapper时需要继承该类
EntityHelper —— 实体类工具类,处理实体和数据库表以及字段关键的一个类
FieldHelper —— 类字段工具类
SqlHelper —— 拼常用SQL的工具类
MapperHelper —— 获取通用mapper的所有配置
自定义Insert Ignore插入
@RegisterMapper
public interface InsertIgnoreSelectiveMapper
@InsertProvider(type = InsertIgnoreSelectiveProvider.class, method = "dynamicSQL")
int insertIgnoreSelective(T record);
}
public class InsertIgnoreSelectiveProvider extends MapperTemplate {
public InsertIgnoreSelectiveProvider(Class> mapperClass, MapperHelper mapperHelper) {
super(mapperClass, mapperHelper);
}
public String insertIgnoreSelective(MappedStatement ms) {
Class> entityClass = getEntityClass(ms);
StringBuilder sql = new StringBuilder();
//获取全部列
Set
EntityColumn logicDeleteColumn = SqlHelper.getLogicDeleteColumn(entityClass);
processKey(sql, entityClass, ms, columnList);
sql.append("INSERT IGNORE INTO ");
sql.append(SqlHelper.getDynamicTableName(entityClass, tableName(entityClass)));
sql.append(" ");
sql.append("
for (EntityColumn column : columnList) {
if (!column.isInsertable()) {
continue;
}
if (column.isIdentity()) {
sql.append(column.getColumn()).append(",");
} else {
if (logicDeleteColumn != null && logicDeleteColumn == column) {
sql.append(column.getColumn()).append(",");
continue;
}
sql.append(SqlHelper.getIfNotNull(column, column.getColumn() + ",", isNotEmpty()));
}
}
sql.append("");
sql.append("
for (EntityColumn column : columnList) {
if (!column.isInsertable()) {
continue;
}
if (logicDeleteColumn != null && logicDeleteColumn == column) {
sql.append(SqlHelper.getLogicDeletedValue(column, false)).append(",");
continue;
}
//优先使用传入的属性值,当原属性property!=null时,用原属性
//自增的情况下,如果默认有值,就会备份到property_cache中,所以这里需要先判断备份的值是否存在
if (column.isIdentity()) {
sql.append(SqlHelper.getIfCacheNotNull(column, column.getColumnHolder(null, "_cache", ",")));
} else {
//其他情况值仍然存在原property中
sql.append(SqlHelper.getIfNotNull(column, column.getColumnHolder(null, null, ","), isNotEmpty()));
}
//当属性为null时,如果存在主键策略,会自动获取值,如果不存在,则使用null
//序列的情况
if (column.isIdentity()) {
sql.append(SqlHelper.getIfCacheIsNull(column, column.getColumnHolder() + ","));
}
}
sql.append("");
return sql.toString();
}
private void processKey(StringBuilder sql, Class> entityClass, MappedStatement ms, Set
//Identity列只能有一个
Boolean hasIdentityKey = false;
//先处理cache或bind节点
for (EntityColumn column : columnList) {
if (column.isIdentity()) {
//这种情况下,如果原先的字段有值,需要先缓存起来,否则就一定会使用自动增长
//这是一个bind节点
sql.append(SqlHelper.getBindCache(column));
//如果是Identity列,就需要插入selectKey
//如果已经存在Identity列,抛出异常
if (hasIdentityKey) {
//jdbc类型只需要添加一次
if (column.getGenerator() != null && "JDBC".equals(column.getGenerator())) {
continue;
}
throw new MapperException(ms.getId() + "对应的实体类" + entityClass.getCanonicalName() + "中包含多个MySql的自动增长列,最多只能有一个!");
}
//插入selectKey
SelectKeyHelper.newSelectKeyMappedStatement(ms, column, entityClass, isBEFORE(), getIDENTITY(column));
hasIdentityKey = true;
} else if(column.getGenIdClass() != null){
sql.append(" sql.append("_parameter").append(", '").append(column.getProperty()).append("'"); sql.append(", @").append(column.getGenIdClass().getCanonicalName()).append("@class"); sql.append(", '").append(tableName(entityClass)).append("'"); sql.append(", '").append(column.getColumn()).append("')"); sql.append("\"/>"); } } } } 八、Example 用法 一、使用MBG生成的Example子类 CountryExample example = new CountryExample(); example.createCriteria().andCountrynameLike("A%"); example.or().andIdGreaterThan(100); example.setDistinct(true); int count = mapper.deleteByExample(example); 二、使用原始的Example类 Example example = new Example(Country.class); example.setForUpdate(true); example.createCriteria().andGreaterThan("id", 100).andLessThan("id",151); example.or().andLessThan("id", 41); List 三、Example.builder 方式 Example example = Example.builder(Country.class) .select("countryname") .where(Sqls.custom().andGreaterThan("id", 100)) .orderByAsc("countrycode") .forUpdate() .build(); List 四、Weekend 方式 List Example.Builder(Country.class) .where(WeekendSqls. .andLike(Country::getCountryname, "%a%") .andGreaterThan(Country::getCountrycode, "123")) .build()); 九、TypeHandler用法 例如:数据库存放的用户地址信息为Hebei/Shijiazhuang、Hebei/Handan 第一步:创建实体类 public class Address implements Serializable { private static final long serialVersionUID = 1L; private String province; private String city; // 省略Getter、Setter方法。。。 @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(); } } public class User implements Serializable { private static final long serialVersionUID = 1L; @Id private Integer id; private String name; @ColumnType(typeHandler = AddressTypeHandler.class) //指定TypeHandler private Address address; //实体类对应的地址 } 第二步:创建对应的TypeHandler public class AddressTypeHandler extends BaseTypeHandler
@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));
}
}
注:上述为局部使用方法、也可以在mybatis-config.xml中配置全局的类型处理器
并在实体类中配置@Column使mybtais将复杂类型作为普通类型向数据库对应字段进行映射
@Table(name = "user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Integer id;
private String name;
@Column
private Address address;
//省略 setter 和 getter
}