通用Mapper
日期:2019-06-25
目录:
- 概述
- 集成(spring-boot)
- 1. 引入依赖包
- 2. 配置yml
- 3. MapperScan
- 应用案例
- 1. 实体类
- 2. 字段类型处理器
- 3. Mapper使用
- 4. 扩展自己Mapper
- 4.1 实现SelectResultProvider类
- 4.2 自动关联Mapper实现
- 5. Weekend动态查询使用
- 通用Mapper实现原理
- 1. Mybatis架构图
- 2. Mapper 原理
概述
通用Mapper就是为了解决单表增删改查,基于Mybatis的插件。开发人员不需要编写SQL,不需要在DAO中增加方法,只要写好实体类,就能支持相应的增删改查方法。
集成(spring-boot)
1. 引入依赖包
-
引用通用Mapper
tk.mybatis mapper-spring-boot-starter 2.1.2
2. 配置yml
mybatis:
type-aliases-package: com.suixingpay.udip.manager.core #领域对象扫描路径
mapper-locations: classpath:mapper/*.xml
type-handlers-package: com.suixingpay.udip.manager.core.handler #字段为枚举类型的Handler
mapper:
mappers:
- com.suixingpay.udip.common.mapper.BaseMapper #mapper的父接口
not-empty: true #insert和update中,是否判断字符串类型!='',少数方法会用到
identity: MYSQL
enum-as-simple-type: true # 允许bean 接受 enum 类型
3. MapperScan
-
mapper扫描路径
@MapperScan(basePackages = "com.suixingpay.udip.manager.core")
说明
- mapperScan为
tk.mybatis.spring.annotation.MapperScan
而不是org.mybatis.spring.annotation.MapperScan
。 - 不能扫描到mapper的基础和自定义接口,比如
com.suixingpay.udip.common.mapper.BaseMapper
等。
应用案例
1. 实体类
-
Person实体类
@Data public class Person implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private Integer age; @ColumnType(typeHandler = BaseHandler.class) private StatusEnum status; @ColumnType(typeHandler = BaseHandler.class) private AuthEnum role; private Long countryId; @Transient private Country country; @Transient private List
personAddresses; } 说明
- 实体字段值映射到数据库字段,采用驼峰字段映射;
- 主键字段使用
@id
注解; - 非数据库字段使用
@Transient
标注; - 枚举类型使用
@ColumnType
注解标注;并指明Hanler处理器;
2. 字段类型处理器
- 通用枚举类型BaseHandler
@MappedJdbcTypes(JdbcType.INTEGER)
@MappedTypes(value = {StatusEnum.class, AuthEnum.class})
public class BaseHandler extends BaseTypeHandler {
private Class types;
public BaseHandler(Class types) {
this.types = types;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, EnumType parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getValue());
}
@Override
public EnumType getNullableResult(ResultSet rs, String columnName) throws SQLException {
int id = rs.getInt(columnName);
if (rs.wasNull()) {
return null;
} else {
return getEnumType(id);
}
}
@Override
public EnumType getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int id = rs.getInt(columnIndex);
if (rs.wasNull()) {
return null;
} else {
return getEnumType(id);
}
}
private EnumType getEnumType(int id) {
try {
Method valueOfType = Arrays.stream(types.getDeclaredMethods())
.filter(m -> m.getName().equals("valueOfType"))
.findFirst()
.orElse(null);
return (EnumType) ReflectionUtils.invokeMethod(valueOfType, types.getEnumConstants()[0], id);
} catch (Exception ex) {
throw new IllegalArgumentException("Cannot convert to " + types.getName() + " by ordinal value.", ex);
}
}
@Override
public EnumType getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int id = cs.getInt(columnIndex);
if (cs.wasNull()) {
return null;
} else {
return getEnumType(id);
}
}
}
- 说明
- class使用注解
@MappedJdbcTypes
和@MappedTypes
,并继承BaseTypeHandler
; - 枚举类要实现接口
EnumType
,该接口valueOfType
用反射来获取实例; - 该类主要就是对
PreparedStatement
和ResultSet
设值和获取值,从数据库到java有个类型映射问题; - 该类型是在
SqlSessionFactoryBean
类中,创建
SqlSessionFactory
时注册字段映射类型;
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
......
if (hasLength(this.typeHandlersPackage)) {
String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
for (String packageToScan : typeHandlersPackageArray) {
configuration.getTypeHandlerRegistry().register(packageToScan);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers");
}
}
}
if (!isEmpty(this.typeHandlers)) {
for (TypeHandler typeHandler : this.typeHandlers) {
configuration.getTypeHandlerRegistry().register(typeHandler);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registered type handler: '" + typeHandler + "'");
}
}
}
......
}
- 该handler会在DefaultResultSetHandlerle类中处理ResultMap时创建返回值的java对象时使用:
public class DefaultResultSetHandler implements ResultSetHandler{
.......
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List> constructorArgTypes, List
- 已经存在的基础类型映射在
SimpleTypeUtil
中;
static {
SIMPLE_TYPE_SET.add(byte[].class);
SIMPLE_TYPE_SET.add(String.class);
SIMPLE_TYPE_SET.add(Byte.class);
SIMPLE_TYPE_SET.add(Short.class);
SIMPLE_TYPE_SET.add(Character.class);
SIMPLE_TYPE_SET.add(Integer.class);
SIMPLE_TYPE_SET.add(Long.class);
SIMPLE_TYPE_SET.add(Float.class);
SIMPLE_TYPE_SET.add(Double.class);
SIMPLE_TYPE_SET.add(Boolean.class);
SIMPLE_TYPE_SET.add(Date.class);
SIMPLE_TYPE_SET.add(Timestamp.class);
SIMPLE_TYPE_SET.add(Class.class);
SIMPLE_TYPE_SET.add(BigInteger.class);
SIMPLE_TYPE_SET.add(BigDecimal.class);
//反射方式设置 java8 中的日期类型
for (String time : JAVA8_DATE_TIME) {
registerSimpleTypeSilence(time);
}
}
3. Mapper使用
- AddresMapper实现
@org.apache.ibatis.annotations.Mapper
public interface AddresMapper extends BaseMapper {
}
- 说明
- 继承BaseMapper,就实现了该类CRUD及复杂查询相关操作;
4. 扩展自己Mapper
@RegisterMapper
public interface ResultMapper {
@SelectProvider(type = SelectResultProvider.class, method = "dynamicSQL")
List selectByExample2Result(Object example);
}
- 说明
- 使用注解
@RegisterMapper
,在创建SqlSessionFactory
时,会自动注入该类; - 该类不能被
MapperScan
扫描到,主要是因为需要获取到范型中实体类型;
4.1 实现SelectResultProvider类
public class SelectResultProvider extends MapperTemplate {
public SelectResultProvider(Class> mapperClass, MapperHelper mapperHelper) {
super(mapperClass, mapperHelper);
}
public String selectByExample2Result(MappedStatement ms) {
Class> entityClass = getEntityClass(ms);
StringBuilder sql = new StringBuilder("SELECT ");
if (isCheckExampleEntityClass()) {
sql.append(SqlHelper.exampleCheck(entityClass));
}
sql.append("distinct ");
//支持查询指定列
sql.append(SqlHelper.exampleSelectColumns(entityClass));
sql.append(SqlHelper.fromTable(entityClass, tableName(entityClass)));
sql.append(SqlHelper.exampleWhereClause());
sql.append(SqlHelper.exampleOrderBy(entityClass));
sql.append(SqlHelper.exampleForUpdate());
return sql.toString();
}
}
- 说明
- 继承
MapperTemplate
,拼装SQL; - 去掉返回类型
setResultType(ms, entityClass)
,而是采用ResultMap("id")进行自动关联查询;
4.2 自动关联Mapper实现
@org.apache.ibatis.annotations.Mapper
public interface PersonMapper extends Mapper{
@Select("select * from person u where u.id = #{id}")
@Results(id = "personResultMap",
value = {
@Result(id = true, property = "id", column = "id"),
@Result(property = "countryId", column = "country_id"),
@Result(property = "country",
column = "country_id",
one = @One(select = "mybatis.example.domain.country.CountryMapper.selectByPrimaryKey", fetchType = FetchType.EAGER))
,
@Result(property = "personAddresses",
column = "id",
many = @Many(select = "mybatis.example.domain.addres.PersonAddressMapper.selectByUserId", fetchType = FetchType.EAGER))
}
)
Person getPersonById(@Param("id") Long id);
@ResultMap("personResultMap")
@SelectProvider(type = SelectResultProvider.class, method = "dynamicSQL")
List selectByExample2Result(Object example);
}
- 说明
- 在方法
selectByExample2Result
上增加注解@ResultMap("personResultMap")
,实现自动关联功能; - 可以根据需要写复杂SQL
@Select("select * from person u where u.id =#{id}")
来实现特殊需求;
5. Weekend动态查询使用
public void selectByExample2Result() {
Weekend of = Weekend.of(Person.class);
of.weekendCriteria()
.andGreaterThan(Person::getAge, 1)
.andLike(Person::getName, "%ndy%");
List list = personMapper.selectByExample2Result(of);
Assert.isTrue(!list.isEmpty(), "list is null");
}
public void weekendSqls() {
Example example = Example.builder(Person.class)
.select(FiledHelper.fieldName(Person::getId),
FiledHelper.fieldName(Person::getName),
FiledHelper.fieldName(Person::getCountryId))
.where(WeekendSqls.custom()
.andLike(Person::getName, "%d%"))
.orWhere(WeekendSqls.custom()
.andGreaterThan(Person::getCountryId, 1)
.andLessThanOrEqualTo(Person::getCountryId, 100))
.build();
List list = personMapper.selectByExample(example);
Assert.isTrue(list.size() > 0, "list is null");
}
- 说明
- Weekend和WeekendSqls实现通用性的包装,通过反射和Lambda表达式,实现声明式接口;
- 不需要写表的字段,而是使用Lambda表达式,简洁友好;
- 生成的动态Sql如下;
SELECT distinct
${selectColumn}
id,name,age,status,role,country_id
FROM person
${@tk.mybatis.mapper.util.OGNL@andNotLogicDelete(_parameter)}
${@tk.mybatis.mapper.util.OGNL@andOr(criteria)}
${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition}
${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition} #{criterion.value}
${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition}
#{listItem}
order by ${orderByClause}
FOR UPDATE
通用Mapper实现原理
1. Mybatis架构图
[图片上传失败...(image-11a4ec-1561497072794)]
- 说明
- SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
- Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
- StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
- ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数,
- ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
- TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
- MappedStatement MappedStatement维护了一条
- SqlSource
负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
BoundSql 表示动态生成的SQL语句以及相应的参数信息 Configuration
MyBatis所有的配置信息都维持在Configuration对象之中。
2. Mapper 原理
- 通用Mapper提供的接口如下:
@RegisterMapper
public interface SelectMapper {
/**
* 根据实体中的属性值进行查询,查询条件使用等号
*
* @param record
* @return
*/
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
List select(T record);
}
- 该接口是使用java范型获取到实体类,通过实体类与数据库的映射,获取到相关字段;也就是说SQL是从实体上动态生成,而不再是读取xml;
如下是通过反射获取到实体类的代码;
public abstract class MapperTemplate{
/**
* 获取返回值类型 - 实体类型
*/
public Class> getEntityClass(MappedStatement ms) {
String msId = ms.getId();
if (entityClassMap.containsKey(msId)) {
return entityClassMap.get(msId);
} else {
Class> mapperClass = getMapperClass(msId);
Type[] types = mapperClass.getGenericInterfaces();
for (Type type : types) {
if (type instanceof ParameterizedType) {
ParameterizedType t = (ParameterizedType) type;
if (t.getRawType() == this.mapperClass || this.mapperClass.isAssignableFrom((Class>) t.getRawType())) {
Class> returnType = (Class>) t.getActualTypeArguments()[0];
//获取该类型后,第一次对该类型进行初始化
EntityHelper.initEntityNameMap(returnType, mapperHelper.getConfig());
entityClassMap.put(msId, returnType);
return returnType;
}
}
}
}
throw new MapperException("无法获取 " + msId + " 方法的泛型信息!");
}
}
- Mybatis中每个方法都会包装成MappedStatement实例,这个对象是对jdbc的statement包装;
这个对象包含id(namespace+id)、结果映射、缓存配置、SqlSource、参数对象等信息;
- Mybatis的在扫描mapper注入mapper时,会解析mapper,并根据该注解@SelectProvider,会生成ProviderSqlSource类,而该类会创建StaticSqlSource来执行SQL;
public class MapperRegistry{
public void addMapper(Class type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
}
- 在MapperAnnotationBuilder类中生成ProviderSqlSource;
public class MapperAnnotationBuilder{
void parseStatement(Method method) {
Class> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method); SqlSource
sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass,languageDriver);
......
}
private SqlSource getSqlSourceFromAnnotations(Method method, Class> parameterType, LanguageDriver languageDriver) {
try {
Class extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
Class extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
if (sqlAnnotationType != null) {
if (sqlProviderAnnotationType != null) {
throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
}
Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
} else if (sqlProviderAnnotationType != null) {
Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
//FIXME 生成ProviderSqlSource类;
return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
}
return null;
} catch (Exception e) {
throw new BuilderException("Could not find value method on SQL annotation. Cause: " + e, e);
}
}
}
- 通用Mapper就是通过ProviderSqlSource生成MappedStatement替换掉静态的StaticSqlSource,而改成可以支持动态的Sql类;
通过MappedStatement类获取到接口和方法,并通反射调用该方法生成动态SQL;用反射替把ProviderSqlSource换成动态Sql;
代码如下:
public SqlSource createSqlSource(MappedStatement ms, String xmlSql) {
return languageDriver.createSqlSource(ms.getConfiguration(), "", null);
}
protected void setSqlSource(MappedStatement ms, SqlSource sqlSource) {
MetaObject msObject = MetaObjectUtil.forObject(ms);
msObject.setValue("sqlSource", sqlSource);
}
-
通用Mapper何时替换ProviderSqlSource
- 初始化
SqlSessionFactory时注册mapper后,通过
configuration.getMappedStatements()获取并循环替换;
- 初始化
-
Spring的情况下,以继承的方式重写了MapperScannerConfigurer 和
MapperFactoryBean,在扫描配置Mapper时Spring 调用 checkDaoConfig
的时候对 SqlSource 进行替换。public class MapperScannerConfigurer{ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { if (this.processPropertyPlaceHolders) { processPropertyPlaceHolders(); } ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); scanner.setAddToConfig(this.addToConfig); scanner.setAnnotationClass(this.annotationClass); scanner.setMarkerInterface(this.markerInterface); scanner.setSqlSessionFactory(this.sqlSessionFactory); scanner.setSqlSessionTemplate(this.sqlSessionTemplate); scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName); scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName); scanner.setResourceLoader(this.applicationContext); scanner.setBeanNameGenerator(this.nameGenerator); scanner.registerFilters(); //设置通用 Mapper scanner.setMapperHelper(this.mapperHelper); scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); } } public class ClassPathMapperScanner{ private MapperFactoryBean> mapperFactoryBean = new MapperFactoryBean