通用mapper
概述
通用mapper可以替我们生成常用增删改查操作的 SQL 语句。
代码官方发布地址:
- https://gitee.com/free
- https://gitee.com/free/Mapper/wikis/1.1-java?parent=1.integration
快速入门
创建测试数据
1) SQL
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');
2) Java实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name="tabple_emp")
public class Employee {
private Integer empId;
private String empName;
private Double empSalary;
private Integer empAge;
}
搭建 MyBatis+Spring 开发环境
mybatis-config.xml spring-context.xml jdbc.properties
集成 Mapper
1)加入Maven依赖信息
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.0.0-beta3</version>
</dependency>
2)修改Spring配置文件
<!-- 整合通用 Mapper 所需要做的配置修改: -->
<!-- 原始全类名:org.mybatis.spring.mapper.MapperScannerConfigurer -->
<!-- 通用 Mapper 使用:tk.mybatis.spring.mapper.MapperScannerConfigurer -->
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.atguigu.mapper.mappers"/>
</bean>
3)创建mapper 和 services
cn.silince.mapper.services;
/**
* 具体操作数据库的 Mapper 接口,需要继承通用 Mapper 提供的核心接口:Mapper<Employee>
* 泛型类型就是实体类的类型
*/
public interface EmployeeMapper extends Mapper<Employee> {
}
package cn.silince.mapper.services;
@Service
public class EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
public Employee getOne(Employee employeeQueryCondition) {
return employeeMapper.selectOne(employeeQueryCondition);
}
}
测试
public class EmployeeMapperTest extends Object {
private ApplicationContext iocContainer = new ClassPathXmlApplicationContext("spring-context.xml");
private EmployeeService employeeService = iocContainer.getBean(EmployeeService.class);
@Test
public void selectOne() {
// 1. 创建封装查询条件的实体类对象
Employee employeeQueryCondition = new Employee(null, "bob", 5560.11, null);
// 2. 执行可查询条件
Employee employeeQueryResult =employeeService.getOne(employeeQueryCondition);
System.out.println(employeeQueryResult);
}
}
常用注解
@Table 注解
作用:建立实体类和数据库表之间的对应关系。
默认规则:实体类类名首字母小写作为表名。Employee 类→employee 表。
用法:在@Table 注解的 name 属性中指定目标数据库表的表名
@Data
@Table(name="tabple_emp")
public class Employee {
private Integer empId;
private String empName;
private Double empSalary;
private Integer empAge;
}
@Column 注解
作用:建立实体类字段和数据库表字段之间的对应关系。
默认规则:
- 实体类字段:驼峰式命名
- 数据库表字段:使用“_”区分各个单词
用法:在@Column 注解的 name 属性中指定目标字段的字段名
@Column(name="emp_salary_apple")
private Double empSalary;
@Id 注解
通用 Mapper 在执行 xxxByPrimaryKey(key)方法时,有两种情况。
- 情况 1:没有使用@Id 注解明确指定主键字段
-- 之所以会生成上面这样的 WHERE 子句是因为通用 Mapper 将实体类中的所有字段都拿来放在一起作为联合主键
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 = ?
-
情况 2:使用@Id 主键明确标记和数据库表中主键字段对应的实体类字段。
@Id private Integer empId;
@GeneratedValue 注解
作用:让通用 Mapper 在执行 insert 操作之后将数据库自动生成的主键值回写到实体类对象中。
自增主键用法:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer empId;
序列主键用法(oracle):
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY,
generator = "select SEQ_ID.nextval from dual")
private Integer empId;
@Transient 主键
用于标记不与数据库表字段对应的实体类字段。
@Transient
private String otherThings; //非数据库表中字段
常用方法
selectOne
实体类封装查询条件生成WHERE子句的规则
- 使用非空的值生成WHERE子句
- 在条件表达式中使用“=”进行比较
要求必须返回一个实体类结果,如果有多个,则会抛出异常
xxxByPrimaryKey
需要使用@Id 主键明确标记和数据库表主键字段对应的实体类字段,否则通用Mapper 会将所有实体类字段作为联合主键。
xxxSelective
非主键字段如果为 null 值,则不加入到 SQL 语句中。
SelectByRowBounds
分页,查询时还是查询全部,在内存中做的分页。真正做分页还是使用pagehelper。
QBC查询
Query By Criteria在 SQL 语句中相当 于查询条件。QBC 查询是将查询条件通过 Java 对象进行模块化封装。
示例代码:
//目标: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 对象
Criteria criteria01 = example.createCriteria();
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);
}
逆向工程
原生 MyBatis 逆向工程和通用 Mapper 逆向工程对比
使用 Maven 执行MBG
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.silince.mapper.mbg</groupId>
<artifactId>Pro02MapperMBG</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- ${basedir}引用工程根目录 -->
<!-- targetJavaProject:声明存放源码的目录位置 -->
<targetJavaProject>${basedir}/src/main/java</targetJavaProject>
<!-- targetMapperPackage:声明MBG生成XxxMapper接口后存放的package位置 -->
<targetMapperPackage>cn.silince.shop.mappers</targetMapperPackage>
<!-- targetModelPackage:声明MBG生成实体类后存放的package位置 -->
<targetModelPackage>cn.silince.shop.entities</targetModelPackage>
<!-- targetResourcesProject:声明存放资源文件和XML配置文件的目录位置 -->
<targetResourcesProject>${basedir}/src/main/resources</targetResourcesProject>
<!-- targetXMLPackage:声明存放具体XxxMapper.xml文件的目录位置 -->
<targetXMLPackage>mappers</targetXMLPackage>
<!-- 通用Mapper的版本号 -->
<mapper.version>4.0.0-beta3</mapper.version>
<!-- MySQL驱动版本号 -->
<mysql.version>8.0.11</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.0.0-beta3</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.8</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<!-- 配置generatorConfig.xml配置文件的路径 -->
<configuration>
<configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<!-- MBG插件的依赖信息 -->
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>${mapper.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
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>
<!-- 引入外部属性文件 -->
<properties resource="config.properties" />
<context id="Mysql" targetRuntime="MyBatis3Simple"
defaultModelType="flat">
<property name="beginningDelimiter" value="`" />
<property name="endingDelimiter" value="`" />
<!-- 配置通用Mapper的MBG插件相关信息 -->
<plugin type="${mapper.plugin}">
<property name="mappers" value="${mapper.Mapper}" />
</plugin>
<!-- 配置连接数据库的基本信息 -->
<jdbcConnection
driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.url}"
userId="${jdbc.user}"
password="${jdbc.password}">
</jdbcConnection>
<!-- 配置Java实体类存放位置 targetModelPackage是pom文件中的属性-->
<javaModelGenerator
targetPackage="${targetModelPackage}"
targetProject="${targetJavaProject}" />
<!-- 配置XxxMapper.xml存放位置 -->
<sqlMapGenerator
targetPackage="${targetXMLPackage}"
targetProject="${targetResourcesProject}" />
<!-- 配置XxxMapper.java存放位置 -->
<javaClientGenerator
targetPackage="${targetMapperPackage}"
targetProject="${targetJavaProject}"
type="XMLMAPPER" />
<!-- 根据数据库表生成Java文件的相关规则 -->
<!-- tableName="%"表示数据库中所有表都参与逆向工程,此时使用默认规则 -->
<!-- 默认规则:table_dept→TableDept -->
<!-- 不符合默认规则时需要使用tableName和domainObjectName两个属性明确指定 -->
<table tableName="tabple_emp" domainObjectName="Employee">
<!-- 配置主键生成策略 -->
<generatedKey column="emp_id" sqlStatement="Mysql" identity="true" />
</table>
</context>
</generatorConfiguration>
config.properties
# Database connection information
jdbc.driverClass = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/commonMapper
jdbc.user = root
jdbc.password = z*****y
#c3p0
jdbc.maxPoolSize=50
jdbc.minPoolSize=10
jdbc.maxStatements=100
jdbc.testConnection=true
# mapper
mapper.plugin = tk.mybatis.mapper.generator.MapperPlugin
mapper.Mapper = tk.mybatis.mapper.common.Mapper
自定义 Mapper<T>接口
tk.mybatis.mapper.common.Mapper
层次结构:
使用步骤
让我们可以根据开发的实际需要对 Mapper<T>
接口进行定制。
1) 创建自定义 Mapper<T>
接口。⚠️MyMapper不能和原来的Mapper放在同一个包下。
public interface MyMapper<T>
extends SelectAllMapper<T>,SelectByExampleMapper<T> {
}
2) 继承该接口
public interface EmployeeMapper extends MyMapper<Employee> {}
3) 配置 MapperScannerConfigurer 注册 MyMapper<T>
<!-- 整合通用Mapper所需要做的配置修改: -->
<!-- 原始全类名:org.mybatis.spring.mapper.MapperScannerConfigurer -->
<!-- 通用Mapper使用:tk.mybatis.spring.mapper.MapperScannerConfigurer -->
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.atguigu.mapper.mappers"/>
<property name="properties">
<value>
mappers=com.atguigu.mapper.mine_mappers.MyMapper
</value>
</property>
</bean>
通用 Mapper 接口扩展
需要提供的接口和实现类
MyBatchUpdateMapper
package cn.silince.mapper.mine_mappers;
public interface MyBatchUpdateMapper<T> {
@UpdateProvider(type=MyBatchUpdateProvider.class,method = "dynamicSQL")
void batchUpdate(List<T> list);
}
MyMapper
package cn.silince.mapper.mine_mappers;
public interface MyMapper<T>
extends SelectAllMapper<T>,
SelectByExampleMapper<T>,
MyBatchUpdateMapper<T> {
}
MyBatchUpdateProvider
package cn.silince.mapper.mine_mappers;
/**
* @program: Pro03MapperOfMine
* @description:
* @author: Silince
* @create: 2020-10-25 22:29
**/
public class MyBatchUpdateProvider extends MapperTemplate {
public MyBatchUpdateProvider(Class<?> mapperClass, MapperHelper mapperHelper) {
super(mapperClass, mapperHelper);
}
/*
<foreach collection="list" item="record" separator=";" >
UPDATE tabple_emp
<set>
emp_name=#{record.empName},
emp_age=#{record.empAge},
emp_salary=#{record.empSalary},
</set>
where emp_id=#{record.empId}
</foreach>
*/
public String batchUpdate(MappedStatement statement){
// 1创建StringBuilder用于拼接SQL语句的各个组成部分
StringBuilder builder = new StringBuilder();
// 2拼接foreach标签
builder.append("<foreach collection=\"list\" item=\"record\" separator=\";\" >");
// 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("<set>");
// 6 获取所有字段的信息
Set<EntityColumn> columns = EntityHelper.getColumns(entityClass);
String idColumn = null;
String idHolder = null;
for (EntityColumn entityColumn : columns) {
// 7 判断主键
boolean isPrimaryKey = entityColumn.isId();
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("</set>");
// 10 使用前面缓存的主键名、主键值拼接where子句
builder.append("where ").append(idColumn).append("=").append(idHolder);
builder.append("</foreach>");
// 11 将拼接好的字符串返回
return builder.toString();
}
}
EmployeeService
package cn.silince.mapper.services;
@Service
public class EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
public List<Employee> getAll() {
return employeeMapper.selectAll();
}
public void batchUpdateEmp(List<Employee> empList){
employeeMapper.batchUpdate(empList);
}
}
二级缓存
1) MyBatis 配置文件开启二级缓存功能
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
2) 在 XxxMapper 接口上使用@CacheNamespace 注解
@CacheNamespace
public interface EmployeeMapper extends MyMapper<Employee> {
}
⚠️:实体类必须实现序列化接口
类型处理器:TypeHandler
通用 Mapper 默认情况下会忽略复杂类型,对复杂类型不进行“从类到表”的映射。
简单类型和复杂类型
基本数据类型:byte short int long double float char boolean
引用数据类型:类、接口、数组、枚举……
简单类型:只有一个值的类型
复杂类型:有多个值的类型
复杂类型 Address 处理
自定义类型转换器
TypeHandler 接口
public interface TypeHandler<T> {
//将 parameter 设置到 ps 对象中,位置是 i
//在这个方法中将 parameter 转换为字符串
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
//根据列名从 ResultSet 中获取数据,通常是字符串形式
//将字符串还原为 Java 对象,以 T 类型返回
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
继承体系
BaseTypeHandler 类中的抽象方法说明
//将 parameter 对象转换为字符串存入到 ps 对象的 i 位置
public abstract void setNonNullParameter(
PreparedStatement ps,
int i,
T parameter,
JdbcType jdbcType) throws SQLException;
//从结果集中获取数据库对应查询结果
//将字符串还原为原始的 T 类型对象
public abstract T getNullableResult(ResultSet rs,String columnName) throws SQLException;
public abstract T getNullableResult( ResultSet rs,int columnIndex) throws SQLException;
public abstract T getNullableResult( CallableStatement cs,int columnIndex) throws SQLException;
自定义类型转换器类
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注解注册MyBatis内置的枚举类型处理器
@ColumnType(typeHandler= AddressTypeHandler.class)
private Address address;
全局级别:在 MyBatis 配置文件中配置 typeHandlers
@Column
private Address address;
@Column
private SeasonEnum season;
<typeHandlers>
<!-- handler属性:指定自定义类型转换器全类名 -->
<!-- javaType属性:指定需要使用“自定义类型转换器”进行类型处理的实体类型 -->
<typeHandler
handler="cn.silince.mapper.handlers.AddressTypeHandler"
javaType="cn.silince.mapper.entities.Address"/>
<typeHandler
handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
javaType="cn.silince.mapper.entities.SeasonEnum"/>
</typeHandlers>
枚举类型的解决办法
在枚举类型中,无法使用@ColumnType注解注册MyBatis内置的枚举类型处理器
方法一:让通用 Mapper 把枚举类型作为简单类型处理。
增加一个通用Mapper的配置项,在 Spring 配置文件中找到 MapperScannerConfigurer。
本质:使用了 org.apache.ibatis.type.EnumTypeHandler<E>
<!-- 整合通用Mapper所需要做的配置修改: -->
<!-- 原始全类名:org.mybatis.spring.mapper.MapperScannerConfigurer -->
<!-- 通用Mapper使用:tk.mybatis.spring.mapper.MapperScannerConfigurer -->
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="cn.silince.mapper.mappers"/>
<property name="properties">
<value>
enumAsSimpleType=true
</value>
</property>
</bean>
方法二:为枚举类型配置对应的类型处理器。
类型处理器:
- 内置
org.apache.ibatis.type.EnumTypeHandler<E>
- 在数据库中存储枚举值本身
org.apache.ibatis.type.EnumOrdinalTypeHandler<E>
- 在数据库中仅仅存储枚举值的索引
- 自定义
- 和address的处理方法完全一样
内置枚举类型处理器注册:
-
不能使用@ColumnType注解
-
需要在 MyBatis 配置文件中配置专门的类型处理器并在字段上使用@Column 注解
- ⚠️ 加@Column 注解的作用是让通用 Mapper 不忽略枚举类型。
<typeHandlers>
<!-- handler属性:指定自定义类型转换器全类名 -->
<!-- javaType属性:指定需要使用“自定义类型转换器”进行类型处理的实体类型 -->
<typeHandler
handler="org.apache.ibatis.type.EnumTypeHandler"
javaType="cn.silince.mapper.entities.SeasonEnum"/>
</typeHandlers>
既已览卷至此,何不品评一二: