通用Mapper
日期:2019-06-25
目錄:
概述
通用Mapper就是為了解決單表增刪改查,基于Mybatis的插件殖蚕。開發(fā)人員不需要編寫SQL霍转,不需要在DAO中增加方法窖梁,只要寫好實體類瘤袖,就能支持相應(yīng)的增刪改查方法挪凑。
集成(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 #領(lǐng)域?qū)ο髵呙杪窂? 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中玖喘,是否判斷字符串類型!=''澎办,少數(shù)方法會用到
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的基礎(chǔ)和自定義接口窖逗,比如
com.suixingpay.udip.common.mapper.BaseMapper
等址否。
應(yīng)用案例
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; }
說明
- 實體字段值映射到數(shù)據(jù)庫字段,采用駝峰字段映射滑负;
- 主鍵字段使用
@id
注解在张; - 非數(shù)據(jù)庫字段使用
@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
; - 枚舉類要實現(xiàn)接口
EnumType
啄骇,該接口valueOfType
用反射來獲取實例痴鳄; - 該類主要就是對
PreparedStatement
和ResultSet
設(shè)值和獲取值,從數(shù)據(jù)庫到j(luò)ava有個類型映射問題缸夹; - 該類型是在
SqlSessionFactoryBean
類中痪寻,創(chuàng)建
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時創(chuàng)建返回值的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);
}
.......
}
- 已經(jīng)存在的基礎(chǔ)類型映射在
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);
//反射方式設(shè)置 java8 中的日期類型
for (String time : JAVA8_DATE_TIME) {
registerSimpleTypeSilence(time);
}
}
3. Mapper使用
- AddresMapper實現(xiàn)
@org.apache.ibatis.annotations.Mapper
public interface AddresMapper extends BaseMapper<Addres> {
}
- 說明
- 繼承BaseMapper橡类,就實現(xiàn)了該類CRUD及復(fù)雜查詢相關(guān)操作;
4. 擴展自己Mapper
@RegisterMapper
public interface ResultMapper<T> {
@SelectProvider(type = SelectResultProvider.class, method = "dynamicSQL")
List<T> selectByExample2Result(Object example);
}
- 說明
- 使用注解
@RegisterMapper
芽唇,在創(chuàng)建SqlSessionFactory
時顾画,會自動注入該類取劫; - 該類不能被
MapperScan
掃描到,主要是因為需要獲取到范型中實體類型研侣;
4.1 實現(xiàn)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")進行自動關(guān)聯(lián)查詢;
4.2 自動關(guān)聯(lián)Mapper實現(xiàn)
@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")
庶诡,實現(xiàn)自動關(guān)聯(lián)功能惦银; - 可以根據(jù)需要寫復(fù)雜SQL
@Select("select * from person u where u.id =#{id}")
來實現(xiàn)特殊需求;
5. Weekend動態(tài)查詢使用
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實現(xiàn)通用性的包裝,通過反射和Lambda表達式末誓,實現(xiàn)聲明式接口扯俱;
- 不需要寫表的字段,而是使用Lambda表達式喇澡,簡潔友好蘸吓;
- 生成的動態(tài)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實現(xiàn)原理
1. Mybatis架構(gòu)圖
[圖片上傳失敗...(image-11a4ec-1561497072794)]
- 說明
- SqlSession 作為MyBatis工作的主要頂層API撩幽,表示和數(shù)據(jù)庫交互的會話库继,完成必要數(shù)據(jù)庫增刪改查功能
- Executor MyBatis執(zhí)行器,是MyBatis 調(diào)度的核心窜醉,負責SQL語句的生成和查詢緩存的維護
- StatementHandler 封裝了JDBC Statement操作宪萄,負責對JDBC statement 的操作,如設(shè)置參數(shù)榨惰、將Statement結(jié)果集轉(zhuǎn)換成List集合拜英。
- ParameterHandler 負責對用戶傳遞的參數(shù)轉(zhuǎn)換成JDBC Statement 所需要的參數(shù),
- ResultSetHandler 負責將JDBC返回的ResultSet結(jié)果集對象轉(zhuǎn)換成List類型的集合琅催;
- TypeHandler 負責java數(shù)據(jù)類型和jdbc數(shù)據(jù)類型之間的映射和轉(zhuǎn)換
- MappedStatement MappedStatement維護了一條<select|update|delete|insert>節(jié)點的封裝居凶,
- SqlSource
負責根據(jù)用戶傳遞的parameterObject揩局,動態(tài)地生成SQL語句匾寝,將信息封裝到BoundSql對象中,并返回
BoundSql 表示動態(tài)生成的SQL語句以及相應(yīng)的參數(shù)信息 Configuration
MyBatis所有的配置信息都維持在Configuration對象之中廉丽。
2. Mapper 原理
- 通用Mapper提供的接口如下:
@RegisterMapper
public interface SelectMapper<T> {
/**
* 根據(jù)實體中的屬性值進行查詢缠黍,查詢條件使用等號
*
* @param record
* @return
*/
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
List<T> select(T record);
}
- 該接口是使用java范型獲取到實體類弄兜,通過實體類與數(shù)據(jù)庫的映射,獲取到相關(guān)字段瓷式;也就是說SQL是從實體上動態(tài)生成替饿,而不再是讀取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)据过、結(jié)果映射颊埃、緩存配置、SqlSource蝶俱、參數(shù)對象等信息班利;
- Mybatis的在掃描mapper注入mapper時,會解析mapper榨呆,并根據(jù)該注解@SelectProvider罗标,會生成ProviderSqlSource類,而該類會創(chuàng)建StaticSqlSource來執(zhí)行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替換掉靜態(tài)的StaticSqlSource竿拆,而改成可以支持動態(tài)的Sql類宙拉;
通過MappedStatement類獲取到接口和方法,并通反射調(diào)用該方法生成動態(tài)SQL丙笋;用反射替把ProviderSqlSource換成動態(tài)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()獲取并循環(huán)替換御板;
- 初始化
-
Spring的情況下锥忿,以繼承的方式重寫了MapperScannerConfigurer 和
MapperFactoryBean,在掃描配置Mapper時Spring 調(diào)用 checkDaoConfig
的時候?qū)?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(); //設(shè)置通用 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){ ...... } }