通用 Mapper是一个可以实现任意 MyBatis 通用方法的框架,项目提供了常规的增删改查操作以及查询相关的单表操作。通用 Mapper 是为了解决 MyBatis 使用中 90% 的基本操作,使用它可以很方便的进行开发,可以节省开发人员大量的时间。
第一步:添加依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>版本号</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>版本号</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>版本号</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>版本号</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>版本号</version>
</dependency>
第二步:开始集成
XML配置有以下两种方式,选其一
使用MapperScannerConfigurer
tk.mybatis.spring.mapper.MapperScannerConfigurer
代替org.mybatis.spring.mapper.MapperScannerConfigurer
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="扫描包名"/>
...
</bean>
使用Configuration
如果某些第三方也需要特殊的 MapperScannerConfigurer
时,就不能用上面的方式进行配置了,此时使用以下方法
<!--使用 Configuration 方式进行配置-->
<bean id="mybatisConfig" class="tk.mybatis.mapper.session.Configuration">
<!-- 配置通用 Mapper,有三种属性注入方式 -->
<property name="mapperProperties">
<value>
notEmpty=true
</value>
</property>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configuration" ref="mybatisConfig"/>
</bean>
<!-- 不需要考虑下面这个,注意这里是 org 的 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="tk.mybatis.mapper.configuration"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
这里使用了 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<Class> mappers = new ArrayList<Class>();
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();
}
}
基于 starter 的自动配置
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>版本号</version>
</dependency>
在 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 {
}
可以设置实体类字段与表字段的转换方式,可选值
public enum Style {
normal, //原值
camelhump, //驼峰转下划线,默认值
uppercase, //转换为大写
lowercase, //转换为小写
camelhumpAndUppercase, //驼峰转下划线大写形式
camelhumpAndLowercase, //驼峰转下划线小写形式
}
作用:建立实体类和数据库表之间的对应关系。 默认规则: 实体类类名首字母小写作为表名。 Employee 类→employee 表 用法: 在@Table注解的 name属性中指定目标数据库表的表名
作用:建立实体类字段和数据库表字段之间的对应关系。
默认规则:实体类字段:驼峰式命名。数据库表字段:使用“_”区分各个单词
用法:在@Column 注解的name 属性中指定目标字段的字段名
主要用于枚举属性,其中column
属性和 @Column 中的 name
作用相同,但是 @Column的优先级更高。除了 name
属性外,这个注解主要提供了 jdbcType
属性和 typeHandler
属性。
jdbcType
用于设置特殊数据库类型时指定数据库中的 jdbcType
。
typeHandler
用于设置特殊类型处理器,常见的是枚举。
用于标记不与数据库表字段对应的实体类字段。对于类中的复杂对象,以及 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 是内置的实现,不是通过拦截器方式实现的,如果版本不一致可能执行影响数为0,但不会抛出异常,所以在 Java6,7中使用时,你需要自己在调用方法后进行判断是否执行成功。
//Java8+可以使用默认方法判断自动抛出异常
public interface MyMapper<T> extends Mapper<T> {
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;
}
}
作用: 通用 Mapper 检测到该接口被继承时,会自动注册。否则需要配置扫描参数。
第一步:实现GenId<T>
接口,实现主键生成策略
//用UUID实现主键生成策略
public class UUIDGenId implements GenId<String> {
@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方式为例
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.kun.mapper.mapper"/>
<property name="properties"><!--设置的是这个properties值-->
<props>
<prop key="keyXXX">valueXXX</prop>
</props>
</property>
</bean>
4.0 之后,增加了一个 @RegisterMapper 注解,通用 Mapper 中提供的所有接口都有这个注解,有了该注解后,通用 Mapper 会自动解析所有的接口,如果父接口(递归向上找到的最顶层)存在标记该注解的接口,就会自动注册上。因此 4.0 后使用通用 Mapper 提供的方法时,不需要在配置这个参数。当自己扩展通用接口时,建议加上该注解,否则就要配置 mappers 参数。
取回主键的方式,可以配置的值为所列的数据库类型,例如MYSQL: SELECT LAST_INSERT_ID()
,配置为IDENTITY=MYSQL
用于配置何时获取主键<selectKey>
中的order
属性,可选值为BEFORE
和AFTER
数据库的catalog
,如果设置该值,查询的时候表名会带catalog
设置的前缀
schema
同catalog
,catalog
优先级高于schema
用于配置是否将枚举类型当成基本类型对待。默认 simpleType 会忽略枚举类型,使用 enumAsSimpleType 配置后会把枚举按简单类型处理,需要自己配置好 typeHandler
。
配置方式如下:enumAsSimpleType=true
用于校验通用 Example
构造参数 entityClass
是否和当前调用的 Mapper<EntityClass>
类型一致,默认 false
。配置该字段为 true
后就会对不匹配的情况进行校验。
配置为 true 后,delete 和 deleteByExample 都必须设置查询条件才能删除,否则会抛出异常
配置为 true 后,updateByExample 和 updateByExampleSelective 都必须设置查询条件才能更新,否则会抛出异常
第一步:引入MBG依赖
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>版本号</version>
<scope>test</scope>
</dependency>
第二步:配置generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- MyBatis3会生成xxxxExample -->
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<!-- 继承的Mapper基类接口 -->
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<!-- caseSensitive默认false,当数据库表名区分大小写时,可以将该属性设置为true -->
<property name="caseSensitive" value="true" />
<!-- 强制打上注解 -->
<property name="forceAnnotation" value="true"/>
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<!-- 是否使用lombok -->
<property name="lombok" value="Data"/>
</plugin>
<!-- 数据库驱动配置 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://192.168.10.191:3306/test"
userId="root"
password="123456">
<!--解决读取别的数据库表问题-->
<property name="nullCatalogMeansCurrent" value="true" />
<!--解决不生成delete、update等-->
<property name="useInformationSchema" value="true"/>
</jdbcConnection>
<!-- java类型处理器
用于处理DB中的类型到Java中的类型,默认使用JavaTypeResolverDefaultImpl;
注意默认会先尝试使用Integer,Long,Short等来对应DECIMAL和 NUMERIC数据类型;
如需配置类型对应关系应该继承它
-->
<!--
<javaTypeResolver type="org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl">
true:使用BigDecimal对应DECIMAL和 NUMERIC数据类型
false:默认,
scale>0;length>18:使用BigDecimal;
scale=0;length[10,18]:使用Long;
scale=0;length[5,9]:使用Integer;
scale=0;length<5:使用Short;
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
-->
<!-- 生成POJO对象所在包 -->
<javaModelGenerator targetPackage="com.kun.model" targetProject="src\main\java">
<!-- POJO类继承的基类,没有可以不写 -->
<property name="rootClass" value="com.kun.model.BaseModel"/>
</javaModelGenerator>
<!-- 生成的mapper.xml所在文件夹 -->
<sqlMapGenerator targetPackage="mapper" targetProject="src\main\resources" />
<!-- 生成的mapper接口所在包 -->
<javaClientGenerator targetPackage="com.kun.mapper" targetProject="src\main\java" type="XMLMAPPER" />
<!-- 表名,主键策略 -->
<table tableName="PRESS_REMI_EXPRESS"
enableCountByExample="false"
enableUpdateByExample="true"
enableDeleteByExample="true"
enableSelectByExample="true"
selectByExampleQueryId="true">
<generatedKey column="ID" sqlStatement="Mysql" identity="true" />
</table>
</context>
</generatorConfiguration>
第三步:以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<String> warnings = new ArrayList<String>();
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<T> {
// 查询全部结果
@SelectProvider(type = MySelectProvider.class, method = "dynamicSQL")
List<T> selectAll();
}
第二步:实现驱动类,注意驱动类继承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();
}
}
注:可映射驱动注解类型
注:对开发自定义有帮助的类
自定义Insert Ignore插入
@RegisterMapper
public interface InsertIgnoreSelectiveMapper<T> {
@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> columnList = EntityHelper.getColumns(entityClass);
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("<trim prefix=\"(\" suffix=\")\" suffixOverrides=\",\">");
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("</trim>");
sql.append("<trim prefix=\"VALUES(\" suffix=\")\" suffixOverrides=\",\">");
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("</trim>");
return sql.toString();
}
private void processKey(StringBuilder sql, Class<?> entityClass, MappedStatement ms, Set<EntityColumn> columnList){
//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("<bind name=\"").append(column.getColumn()).append("GenIdBind\" value=\"@tk.mybatis.mapper.genid.GenIdUtil@genId(");
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("\"/>");
}
}
}
}
一、使用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<Country> countries = mapper.selectByExample(example);
三、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);
四、Weekend 方式
List<Country> selectByWeekendSql = mapper.selectByExample(
Example.Builder(Country.class)
.where(WeekendSqls.<Country>custom()
.andLike(Country::getCountryname, "%a%")
.andGreaterThan(Country::getCountrycode, "123"))
.build());
例如:数据库存放的用户地址信息为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<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));
}
}
注:上述为局部使用方法、也可以在mybatis-config.xml中配置全局的类型处理器
<typeHandlers>
<typeHandler handler="tk.mybatis.mapper.typehandler.AddressTypeHandler"/>
</typeHandlers>
并在实体类中配置@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
}