通用Mapper
日期:2019-06-25
目录:
概述
通用Mapper就是为了解决单表增删改查,基于Mybatis的插件。开发人员不需要编写SQL,不需要在DAO中增加方法,只要写好实体类,就能支持相应的增删改查方法。
集成(spring-boot)
1. 引入依赖包
-
引用通用Mapper
<dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.1.2</version> </dependency>
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<PersonAddress> personAddresses; }
-
说明
- 实体字段值映射到数据库字段,采用驼峰字段映射;
- 主键字段使用
@id
注解; - 非数据库字段使用
@Transient
标注; - 枚举类型使用
@ColumnType
注解标注;并指明Hanler处理器;
2. 字段类型处理器
- 通用枚举类型BaseHandler
@MappedJdbcTypes(JdbcType.INTEGER)
@MappedTypes(value = {StatusEnum.class, AuthEnum.class})
public class BaseHandler extends BaseTypeHandler<EnumType> {
private Class<EnumType> types;
public BaseHandler(Class<EnumType> 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
; - 枚举类要实现接口
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<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix) throws SQLException {
final Class<?> resultType = resultMap.getType();
final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
if (hasTypeHandlerForResultObject(rsw, resultType)) {
return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
} else if (!constructorMappings.isEmpty()) {
return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
} else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
return objectFactory.create(resultType);
} else if (shouldApplyAutomaticMappings(resultMap, false)) {
return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix);
}
throw new ExecutorException("Do not know how to create an instance of " + resultType);
}
.......
}
- 已经存在的基础类型映射在
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<Addres> {
}
- 说明
- 继承BaseMapper,就实现了该类CRUD及复杂查询相关操作;
4. 扩展自己Mapper
@RegisterMapper
public interface ResultMapper<T> {
@SelectProvider(type = SelectResultProvider.class, method = "dynamicSQL")
List<T> 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("<if test=\"distinct\">distinct</if>");
//支持查询指定列
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<Person>{
@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<Person> selectByExample2Result(Object example);
}
- 说明
- 在方法
selectByExample2Result
上增加注解@ResultMap("personResultMap")
,实现自动关联功能; - 可以根据需要写复杂SQL
@Select("select * from person u where u.id =#{id}")
来实现特殊需求;
5. Weekend动态查询使用
public void selectByExample2Result() {
Weekend<Person> of = Weekend.of(Person.class);
of.weekendCriteria()
.andGreaterThan(Person::getAge, 1)
.andLike(Person::getName, "%ndy%");
List<Person> 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.<Person>custom()
.andLike(Person::getName, "%d%"))
.orWhere(WeekendSqls.<Person>custom()
.andGreaterThan(Person::getCountryId, 1)
.andLessThanOrEqualTo(Person::getCountryId, 100))
.build();
List<Person> list = personMapper.selectByExample(example);
Assert.isTrue(list.size() > 0, "list is null");
}
- 说明
- Weekend和WeekendSqls实现通用性的包装,通过反射和Lambda表达式,实现声明式接口;
- 不需要写表的字段,而是使用Lambda表达式,简洁友好;
- 生成的动态Sql如下;
SELECT <if test="distinct">distinct</if>
<choose>
<when test="@tk.mybatis.mapper.util.OGNL@hasSelectColumns(_parameter)">
<foreach collection="_parameter.selectColumns" item="selectColumn" separator=",">${selectColumn}</foreach>
</when>
<otherwise>id,name,age,status,role,country_id</otherwise>
</choose> FROM person
<if test="_parameter != null">
<where>${@tk.mybatis.mapper.util.OGNL@andNotLogicDelete(_parameter)} <trim prefix="(" prefixOverrides="and |or " suffix=")">
<foreach collection="oredCriteria" item="criteria">
<if test="criteria.valid">
${@tk.mybatis.mapper.util.OGNL@andOr(criteria)} <trim prefix="(" prefixOverrides="and |or " suffix=")">
<foreach collection="criteria.criteria" item="criterion">
<choose>
<when test="criterion.noValue">
${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition}
</when>
<when test="criterion.singleValue">
${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue">
${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue">
${@tk.mybatis.mapper.util.OGNL@andOr(criterion)} ${criterion.condition}
<foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</trim>
</where>
</if>
<if test="orderByClause != null">order by ${orderByClause}</if>
<if test="@tk.mybatis.mapper.util.OGNL@hasForUpdate(_parameter)">FOR UPDATE</if>
通用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维护了一条<select|update|delete|insert>节点的封装,
- SqlSource
负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
BoundSql 表示动态生成的SQL语句以及相应的参数信息 Configuration
MyBatis所有的配置信息都维持在Configuration对象之中。
2. Mapper 原理
- 通用Mapper提供的接口如下:
@RegisterMapper
public interface SelectMapper<T> {
/**
* 根据实体中的属性值进行查询,查询条件使用等号
*
* @param record
* @return
*/
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
List<T> 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 <T> void addMapper(Class<T> 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<T>(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(), "<script>\n\t" + xmlSql + "</script>", 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<Object>(); private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions){ ...... } }
网友评论