Mybati虽然极大的提高了程序员对数据库的操作, 但还是存在以下痛点:
1、mapper.xml文件里有大量的sql,当数据库表字段变动,配置文件就要修改;
2、需要自己实现sql分页,select * from table where . . . limit 1,3;
3、数据库可移植性差:如果项目更换数据库,比如oracle–>mysql,mapper.xml中的sql要重新写,因为Oracle的PLSQL 和mysql 支持的函数是不同的;
4、生成的代码量过大;
5、批量操作,批量插入,批量更新,需要自写。
通用mapper的作用就是自动生成我们常用的增删改查SQL语句。是中国程序员在 MBG 的基础上结合了部分 JPA 注解做出来的。
Github地址:https://gitee.com/free/Mapper.git
先创建一个数据库用于案例测试:
CREATE TABLE `tabple_emp` (
`emp_id` int NOT NULL AUTO_INCREMENT ,
`emp_name` varchar(500) NULL ,
`emp_salary` double(15,5) NULL ,
`emp_age` int NULL ,
PRIMARY KEY (`emp_id`)
);
INSERT INTO `tabple_emp` (`emp_name`, `emp_salary`, `emp_age`) VALUES ('tom', '1254.37', '27');
INSERT INTO `tabple_emp` (`emp_name`, `emp_salary`, `emp_age`) VALUES ('jerry', '6635.42', '38');
INSERT INTO `tabple_emp` (`emp_name`, `emp_salary`, `emp_age`) VALUES ('bob', '5560.11', '40');
INSERT INTO `tabple_emp` (`emp_name`, `emp_salary`, `emp_age`) VALUES ('kate', '2209.11', '22');
INSERT INTO `tabple_emp` (`emp_name`, `emp_salary`, `emp_age`) VALUES ('justin', '4203.15', '30');
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapperartifactId>
<version>最新版本version>
dependency>
public class Employee {
private Integer empId;
private String empName;
private Double empSalary;
private Integer empAge;
// ..有参构造、无参构造、set、get、toString
}
import tk.mybatis.mapper.common.Mapper;
public interface EmployeeMapper extends Mapper<Employee> {
}
这里继承了 tk.mybatis.mapper.common.Mapper
接口,在接口上指定了泛型类型 Employee。当你继承了 Mapper 接口后,此时就已经有了针对 Employee的大量方法,方法如下:
为了让我们自己创建的Mapper成为通用Mapper,让项目在启动的时候,把上述方法都自动生成好,这样在运行时就可以使用上面所有的方法。
根据不同的开发环境,需要不同的配置方式,完整的内容可以 在https://github.com/abel533/Mapper/wiki/1.integration中查看文档,我们这里以最常见的 Spring 和 MyBatis 集成为例。
在集成 Spring 的环境中使用 MyBatis 接口方式时,需要配置 MapperScannerConfigurer,在这种情况下使用通用 Mapper,只需要修改spring配置文件如下:
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="cn.klb.mapper.mappers"/>
bean>
的value为我们创建的Mapper的所在包。
编写测试方法来访问数据库:
service:
@Service
public class EmployeeService {
@Autowired
public EmployeeMapper employeeMapper;
public Employee getOne(Employee employeeQueryCondition) {
return employeeMapper.selectOne(employeeQueryCondition);
}
}
public class EmployeeMapperTest {
private ApplicationContext iocContainer = new ClassPathXmlApplicationContext("spring-context.xml");
private EmployeeService employeeService = iocContainer.getBean(EmployeeService.class);
@Test
public void testSelectOne() {
//1.创建封装查询条件的实体类对象
Employee employeeQueryCondition = new Employee(null, "bob", 5560.11, null);
//2.执行查询
Employee employeeQueryResult = employeeService.getOne(employeeQueryCondition);
//3.打印
System.out.println(employeeQueryResult);
}
}
作用:建立实体类和数据库表之间的对应关系。
默认规则:实体类类名首字母小写作为表名。Employee 类→employee 表。
用法:在@Table 注解的 name 属性中指定目标数据库表的表名。
作用:建立实体类字段和数据库表字段之间的对应关系。
默认规则:
实体类字段:驼峰式命名
数据库表字段:使用“_”区分各个单词
用法:在@Column 注解的 name 属性中指定目标字段的字段名。
通用 Mapper 在执行 xxxByPrimaryKey(key)
方法时,有两种情况:
情况 1:没有使用@Id 注解明确指定主键字段,生成的sql语句如下:
SELECT emp_id,emp_name,emp_salary_apple,emp_age FROM tabple_emp WHERE emp_id = ? AND emp_name = ? AND emp_salary_apple = ? AND emp_age = ?
之所以会生成上面这样的 WHERE 子句是因为通用 Mapper 将实体类中的所有字段都拿来放在一起作为联合主键。
情况 2:使用@Id
主键明确标记和数据库表中主键字段对应的实体类字段。
作用:让通用 Mapper 在执行 insert 操作之后将数据库自动生成的主键值回写到实体类对象中。
自增主键用法:
序列主键用法:
用于标记不与数据库表字段对应的实体类字段。
@Transient
private String otherThings; //非数据库表中字段
通用 Mapper 替我们自动生成的 SQL 语句情况:
实体类封装查询条件生成 WHERE 子句的规则:
1、使用非空的值生成 WHERE 子句;
2、在条件表达式中使用“=”进行比较。
需要使用@Id
主键明确标记和数据库表主键字段对应的实体类字段,否则通用Mapper 会将所有实体类字段作为联合主键。
非主键字段如果为 null 值,则不加入到 SQL 语句中。
QBC全称为Query By Criteria,Criteria 是 Criterion 的复数形式。意思是:规则、标准、准则。在 SQL 语句中相当于查询条件。
QBC 查询是将查询条件通过 Java 对象进行模块化封装。
示例代码:
@Test
public void testSelectByExample() {
//目标:WHERE (emp_salary>? AND emp_age) OR (emp_salary AND emp_age>?)
//1.创建Example对象
Example example = new Example(Employee.class);
//***********************
//i.设置排序信息
example.orderBy("empSalary").asc().orderBy("empAge").desc();
//ii.设置“去重”
example.setDistinct(true);
//iii.设置select字段
example.selectProperties("empName","empSalary");
//***********************
//2.通过Example对象创建Criteria对象
Example.Criteria criteria01 = example.createCriteria();
Example.Criteria criteria02 = example.createCriteria();
//3.在两个Criteria对象中分别设置查询条件
//property参数:实体类的属性名
//value参数:实体类的属性值
criteria01.andGreaterThan("empSalary", 3000)
.andLessThan("empAge", 25);
criteria02.andLessThan("empSalary", 5000)
.andGreaterThan("empAge", 30);
//4.使用OR关键词组装两个Criteria对象
example.or(criteria02);
//5.执行查询
List<Employee> empList = employeeService.getEmpListByExample(example);
for (Employee employee : empList) {
System.out.println(employee);
}
}
MBG全称为Mybatis Generator,用于根据数据表生成Mybatis所需的实体类、配置文件和Mapper接口。
通用 Mapper 专用代码生成器生成的 Model 会在原有基础上增加 @Table
,@Id
,@Column
等注解,方便自动会数据库字段进行映射。
参考文档:https://github.com/abel533/Mapper/wiki/4.1.mappergenerator
<dependency>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-coreartifactId>
<version>1.3.6version>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapperartifactId>
<version>4.0.0version>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapper-generatorartifactId>
<version>1.0.0version>
dependency>
同时在pom.xml文件中配置一些属性:
<properties>
<targetJavaProject>${basedir}/src/main/javatargetJavaProject>
<targetMapperPackage>cn.klb.shop.mapperstargetMapperPackage>
<targetModelPackage>cn.klb.shop.entitiestargetModelPackage>
<targetResourcesProject>${basedir}/src/main/resourcestargetResourcesProject>
<targetXMLPackage>mapperstargetXMLPackage>
<mapper.version>4.0.0-beta3mapper.version>
<mysql.version>5.1.37mysql.version>
properties>
<generatorConfiguration>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`" />
<property name="endingDelimiter" value="`" />
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<property name="caseSensitive" value="true"/>
<property name="forceAnnotation" value="true"/>
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
plugin>
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/common_mapper?useUnicode=true&characterEncoding=utf8"
userId="root"
password="root">
jdbcConnection>
<javaModelGenerator
targetPackage="${targetModelPackage}"
targetProject="${targetJavaProject}" />
<sqlMapGenerator
targetPackage="${targetXMLPackage}"
targetProject="${targetResourcesProject}" />
<javaClientGenerator
targetPackage="${targetMapperPackage}"
targetProject="${targetJavaProject}"
type="XMLMAPPER" />
<table tableName="tabple_emp" domainObjectName="Employee">
<generatedKey column="emp_id" sqlStatement="Mysql" identity="true" />
table>
context>
generatorConfiguration>
配置插件很简单,就是配置maven插件,在pom.xml文件中添加:
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-maven-pluginartifactId>
<version>1.3.6version>
<configuration>
<configurationFile>
${basedir}/src/main/resources/generator/generatorConfig.xml
configurationFile>
<overwrite>trueoverwrite>
<verbose>trueverbose>
configuration>
<dependencies>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.29version>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapperartifactId>
<version>4.0.0version>
dependency>
dependencies>
plugin>
plugins>
build>
我们可以根据开发的实际需要对 Mapper接口进行定制。
下图是tk.mybatis.mapper.common.Mapper
的继承图:
有时候我们的业务不需要那么多预提供的方法,所以我们可以继承相应的父接口。
package cn.klb.mapper.mine_mappers;
import tk.mybatis.mapper.common.base.select.SelectAllMapper;
import tk.mybatis.mapper.common.example.SelectByExampleMapper;
public interface MyMapper<T>
extends SelectAllMapper<T>,SelectByExampleMapper<T> {
}
使用自定义的Mapper:
package cn.klb.mapper.mappers;
import cn.klb.mapper.entities.Employee;
import cn.klb.mapper.mine_mappers.MyMapper;
public interface EmployeeMapper extends MyMapper<Employee> {
}
spring的配置文件中:
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="cn.klb.mapper.mappers"/>
bean>
修改为:
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="cn.klb.mapper.mappers"/>
<property name="properties">
<value>
mappers=cn.klb.mapper.mine_mappers.MyMapper
value>
property>
bean>
自定义Mapper是对已提供的方法进行重新组合了一下,不属于拓展。
下面来自定义一个Mapper实现批量更新操作。
拓展步骤如下:
<dependencies>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapperartifactId>
<version>4.0.0-beta3version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.9version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
<dependency>
<groupId>cglibgroupId>
<artifactId>cglibartifactId>
<version>2.2version>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.6.8version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
<version>1.7.7version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.7version>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.2.8version>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatis-springartifactId>
<version>1.2.2version>
dependency>
<dependency>
<groupId>com.mchangegroupId>
<artifactId>c3p0artifactId>
<version>0.9.2version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>4.3.10.RELEASEversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.37version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-ormartifactId>
<version>4.3.10.RELEASEversion>
dependency>
dependencies>
自定义一个类,继承tk.mybatis.mapper.mapperhelper.MapperTemplate
:
package cn.klb.mapper.mine_mappers;
import java.util.Set;
import org.apache.ibatis.mapping.MappedStatement;
import tk.mybatis.mapper.entity.EntityColumn;
import tk.mybatis.mapper.mapperhelper.EntityHelper;
import tk.mybatis.mapper.mapperhelper.MapperHelper;
import tk.mybatis.mapper.mapperhelper.MapperTemplate;
import tk.mybatis.mapper.mapperhelper.SqlHelper;
public class MyBatchUpdateProvider extends MapperTemplate {
public MyBatchUpdateProvider(Class<?> mapperClass, MapperHelper mapperHelper) {
super(mapperClass, mapperHelper);
}
public String batchUpdate(MappedStatement statement) {
//1.创建StringBuilder用于拼接SQL语句的各个组成部分
StringBuilder builder = new StringBuilder();
//2.拼接foreach标签
builder.append("" );
//3.获取实体类对应的Class对象
Class<?> entityClass = super.getEntityClass(statement);
//4.获取实体类在数据库中对应的表名
String tableName = super.tableName(entityClass);
//5.生成update子句
String updateClause = SqlHelper.updateTable(entityClass, tableName);
builder.append(updateClause);
builder.append("" );
//6.获取所有字段信息
Set<EntityColumn> columns = EntityHelper.getColumns(entityClass);
String idColumn = null;
String idHolder = null;
for (EntityColumn entityColumn : columns) {
boolean isPrimaryKey = entityColumn.isId();
//7.判断当前字段是否为主键
if(isPrimaryKey) {
//8.缓存主键的字段名和字段值
idColumn = entityColumn.getColumn();
//※返回格式如:#{record.age,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
idHolder = entityColumn.getColumnHolder("record");
}else {
//9.使用非主键字段拼接SET子句
String column = entityColumn.getColumn();
String columnHolder = entityColumn.getColumnHolder("record");
builder.append(column).append("=").append(columnHolder).append(",");
}
}
builder.append("");
//10.使用前面缓存的主键名、主键值拼接where子句
builder.append("where ").append(idColumn).append("=").append(idHolder);
builder.append("");
//11.将拼接好的字符串返回
return builder.toString();
}
}
其中最关键的就是batchUpdate
方法,里面编写了批量更新的逻辑,其实就是以前的mapper.xml中的sql语句拼接。
自定义一个Mapper,里面包含一个名字为batchUpdate
的方法,并使用@UpdateProvider
注解,属性type的值为第一步定义的类的类对象,method属性的值为固定值dynamicSQL
:
package cn.klb.mapper.mine_mappers;
import java.util.List;
import org.apache.ibatis.annotations.UpdateProvider;
public interface MyBatchUpdateMapper<T> {
@UpdateProvider(type=MyBatchUpdateProvider.class, method="dynamicSQL")
void batchUpdate(List<T> list);
}
上面一步写好的自定义Mapper后,就可以按照通用Mapper的方式来使用,首先是继承这个自定义的Mapper:
package cn.klb.mapper.mine_mappers;
import tk.mybatis.mapper.common.base.select.SelectAllMapper;
import tk.mybatis.mapper.common.example.SelectByExampleMapper;
public interface MyMapper<T>
extends SelectAllMapper<T>,SelectByExampleMapper<T>,MyBatchUpdateMapper<T> {
}
然后使用MyMapper:
package cn.klb.mapper.mappers;
import cn.klb.mapper.entities.Employee;
import cn.klb.mapper.mine_mappers.MyMapper;
public interface EmployeeMapper extends MyMapper<Employee> {
}
然后修改spring的配置文件:
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="cn.klb.mapper.mappers"/>
<property name="properties">
<value>
mappers=cn.klb.mapper.mine_mappers.MyMapper
value>
property>
bean>
编写测试类:
@Test
public void testBatch(){
ClassPathXmlApplicationContext iocContainer = new ClassPathXmlApplicationContext("spring-context.xml");
EmployeeService employeeService = iocContainer.getBean(EmployeeService.class);
List<Employee> empList = new ArrayList<>();
empList.add(new Employee(9, "newName01", 111.11, 10));
empList.add(new Employee(10, "newName02", 222.22, 20));
empList.add(new Employee(11, "newName03", 333.33, 30));
employeeService.batchUpdateEmp(empList);
iocContainer.close();
}
1、MyBatis 配置文件开启二级缓存功能
<settings>
<setting name="cacheEnabled" value="true"/>
settings>
2、 在 XxxMapper 接口上使用@CacheNamespace
注解:
@CacheNamespace
public interface EmployeeMapper extends MyMapper<Employee> {
}
如果我们的数据表对应的实体类是复杂类型:
@Table(name="table_user")
public class User {
@Id
private Integer userId;
private String userName;
private Address address;
private SeasonEnum season;
// set\get\toString...
}
其中Address
是复杂类型:
public class Address {
private String province;
private String city;
private String street;
// set\get\toString...
}
通用 Mapper 默认情况下会忽略复杂类型,对复杂类型不进行“从类到表”的映射。也就是说,如果实体类存在复杂类型,会赋值失败。
解决方法就是使用类型处理器。
自定义的类型处理器需要继承org.apache.ibatis.type.BaseTypeHandler
:
package cn.klb.mapper.handlers;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import cn.klb.mapper.entities.Address;
public class AddressTypeHandler extends BaseTypeHandler<Address> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Address address, JdbcType jdbcType) throws SQLException {
//1.对address对象进行验证
if(address == null) {
return ;
}
//2.从address对象中取出具体数据
String province = address.getProvince();
String city = address.getCity();
String street = address.getStreet();
//3.拼装成一个字符串
//规则:各个值之间使用“,”分开
StringBuilder builder = new StringBuilder();
builder.append(province).append(",").append(city).append(",").append(street);
String parameterValue = builder.toString();
//4.设置参数
ps.setString(i, parameterValue);
}
@Override
public Address getNullableResult(ResultSet rs, String columnName) throws SQLException {
//1.根据字段名从rs对象中获取字段值
String columnValue = rs.getString(columnName);
//2.验证columnValue是否有效
if(columnValue == null || columnValue.length() == 0 || !columnValue.contains(",")) {
return null;
}
//3.根据“,”对columnValue进行拆分
String[] split = columnValue.split(",");
//4.从拆分结果数组中获取Address需要的具体数据
String province = split[0];
String city = split[1];
String street = split[2];
//5.根据具体对象组装一个Address对象
Address address = new Address(province, city, street);
return address;
}
@Override
public Address getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
//1.根据字段名从rs对象中获取字段值
String columnValue = rs.getString(columnIndex);
//2.验证columnValue是否有效
if(columnValue == null || columnValue.length() == 0 || !columnValue.contains(",")) {
return null;
}
//3.根据“,”对columnValue进行拆分
String[] split = columnValue.split(",");
//4.从拆分结果数组中获取Address需要的具体数据
String province = split[0];
String city = split[1];
String street = split[2];
//5.根据具体对象组装一个Address对象
Address address = new Address(province, city, street);
return address;
}
@Override
public Address getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
//1.根据字段名从rs对象中获取字段值
String columnValue = cs.getString(columnIndex);
//2.验证columnValue是否有效
if(columnValue == null || columnValue.length() == 0 || !columnValue.contains(",")) {
return null;
}
//3.根据“,”对columnValue进行拆分
String[] split = columnValue.split(",");
//4.从拆分结果数组中获取Address需要的具体数据
String province = split[0];
String city = split[1];
String street = split[2];
//5.根据具体对象组装一个Address对象
Address address = new Address(province, city, street);
return address;
}
}
方法一(字段级别):在对应复杂属性上加@ColumnType
:
@ColumnType(typeHandler=AddressTypeHandler.class)
private Address address;
方法二(全局):在 MyBatis 配置文件中配置 typeHandlers:
<typeHandlers>
<typeHandler
handler="cn.klb.mapper.handlers.AddressTypeHandler"
javaType="cn.klb.mapper.entities.Address"/>
typeHandlers>
让通用 Mapper 把枚举类型作为简单类型处理,增加一个通用 Mapper 的配置项,在 Spring 配置文件中找到 MapperScannerConfigurer:
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="cn.klb.mapper.mappers"/>
<property name="properties">
<value>
enumAsSimpleType=true
value>
property>
bean>
本质上就是使用了org.apache.ibatis.type.EnumTypeHandler
。
为枚举类型配置对应的类型处理器,和复杂类型一模一样操作。