前言
以前公司用的jpa, 我的感受很方便, 新的項目選擇使用mybatis, sql都是寫在xml文件裏, 雖然基本的方法都有工具生成, 可是一旦數據增長一個字段, 修改這些方法真的是不爽, 並且我的看xml文件感受是真的累, 就這樣不爽裏一段時間, 趁着項目空閒的時候, 研究下如何拋棄xml文件, 徹底使用註解的方式, 而且把通用的方法抽出到一個基類中。java
本文代碼已整理上傳githubgit
通用mapper通常包含基本的增刪改, 根據id查, 根據某一個屬性查, 根據條件集合查 這些方法。
使用的時候直接繼承, 泛型就是具體的實體類。
mybatis 提供了@InsertProvider, @SelectProvider等來動態生成sql, 因此通用mapper就是使用的這些註解。github
通用mapper動態生成sql的思路就是拿到實體類的class, 根據class解析出對應表的元數據, 包括表名, 主鍵信息, 數據庫字段等, 在根據這些信息動態生成sql。spring
對於insert和update來講, 方法的參數就是實體對象, 直接getClass()就能拿到, 可是對於查詢和刪除,方法的參數就不是實體對象了, 在通用mapper裏怎麼拿到 class對象
, mybatis 3.4.5 以前的版本是作不到的, 從源碼裏就限制了, 參考mybatis的這個issue, 3.4.5版本開始, 在調用provider方法時 能夠多傳遞一個參數-ProviderContext, 這個ProviderContext 就能夠獲取當前具體是哪一個mapper的class和調用的方法。sql
這樣經過 具體mapper的接口獲取到泛型參數, 這個泛型參數就是實體對象, 就是 T 的具體值
下面是具體的實現, 使用mybatis的版本是 3.4.6, 依賴了 spring的一些工具類數據庫
public interface BaseMapper<Entity> { /** * 新增一條記錄 * * @param entity 實體 * @return 受影響記錄 */ @InsertProvider(type = BaseSqlProvider.class, method = "insert") @Options(useGeneratedKeys = true, keyColumn = "id") int insert(Entity entity); /** * 更新一條記錄 * * @param entity entity * @return 受影響記錄 */ @UpdateProvider(type = BaseSqlProvider.class, method = "update") int update(Entity entity); /** * 刪除一條記錄 * * @param id id * @return 受影響記錄 */ @DeleteProvider(type = BaseSqlProvider.class, method = "delete") int delete(Long id); /** * 根據id查詢 * * @param id id * @return Entity */ @SelectProvider(type = BaseSqlProvider.class, method = "selectById") Entity selectById(Long id); /** * 根據屬性查詢一條記錄 * * @param function property * @param value value * @param <R> R * @return Entity */ @SelectProvider(type = BaseSqlProvider.class, method = "selectByProperty") <R> Entity selectByProperty(@Param("property") PropertyFunction<Entity, R> function, @Param("value") Object value); /** * 根據屬性查詢記錄列表 * * @param function property * @param value value * @param <R> R * @return Entity */ @SelectProvider(type = BaseSqlProvider.class, method = "selectByProperty") <R> List<Entity> selectListByProperty(@Param("property") PropertyFunction<Entity, R> function, @Param("value") Object value); /** * 根據查詢條件查詢記錄 * * @param condition condition * @param <Condition> Condition * @return List Entity */ @SelectProvider(type = BaseSqlProvider.class, method = "selectByCondition") <Condition> List<Entity> selectByCondition(Condition condition); }
public class BaseSqlProvider { public <Entity> String insert(Entity entity) { Assert.notNull(entity, "entity must not null"); Class<?> entityClass = entity.getClass(); TableMataDate mataDate = TableMataDate.forClass(entityClass); Map<String, String> fieldColumnMap = mataDate.getFieldColumnMap(); SQL sql = new SQL(); sql.INSERT_INTO(mataDate.getTableName()); for (Map.Entry<String, String> entry : fieldColumnMap.entrySet()) { // 忽略主鍵 if (Objects.equals(entry.getKey(), mataDate.getPkProperty())) { continue; } PropertyDescriptor ps = BeanUtils.getPropertyDescriptor(entityClass, entry.getKey()); if (ps == null || ps.getReadMethod() == null) { continue; } Object value = ReflectionUtils.invokeMethod(ps.getReadMethod(), entity); if (!StringUtils.isEmpty(value)) { sql.VALUES(entry.getValue(), getTokenParam(entry.getKey())); } } return sql.toString(); } public <Entity> String update(Entity entity) { Assert.notNull(entity, "entity must not null"); Class<?> entityClass = entity.getClass(); TableMataDate mataDate = TableMataDate.forClass(entityClass); Map<String, String> fieldColumnMap = mataDate.getFieldColumnMap(); SQL sql = new SQL(); sql.UPDATE(mataDate.getTableName()); for (Map.Entry<String, String> entry : fieldColumnMap.entrySet()) { // 忽略主鍵 if (Objects.equals(entry.getKey(), mataDate.getPkProperty())) { continue; } PropertyDescriptor ps = BeanUtils.getPropertyDescriptor(entityClass, entry.getKey()); if (ps == null || ps.getReadMethod() == null) { continue; } Object value = ReflectionUtils.invokeMethod(ps.getReadMethod(), entity); if (!StringUtils.isEmpty(value)) { sql.SET(getEquals(entry.getValue(), entry.getKey())); } } return sql.WHERE(getEquals(mataDate.getPkColumn(), mataDate.getPkProperty())).toString(); } public String delete(ProviderContext context) { Class<?> entityClass = getEntityClass(context); TableMataDate mataDate = TableMataDate.forClass(entityClass); return new SQL().DELETE_FROM(mataDate.getTableName()) .WHERE(getEquals(mataDate.getPkColumn(), mataDate.getPkProperty())) .toString(); } public String selectById(ProviderContext context) { Class<?> entityClass = getEntityClass(context); TableMataDate mataDate = TableMataDate.forClass(entityClass); return new SQL().SELECT(mataDate.getBaseColumns()) .FROM(mataDate.getTableName()) .WHERE(getEquals(mataDate.getPkColumn(), mataDate.getPkProperty())) .toString(); } public String selectByProperty(ProviderContext context, Map<String, Object> params) { PropertyFunction propertyFunction = (PropertyFunction) params.get("property"); String property = SerializedLambdaUtils.getProperty(propertyFunction); Class<?> entityClass = getEntityClass(context); TableMataDate mataDate = TableMataDate.forClass(entityClass); String column = mataDate.getFieldColumnMap().get(property); return new SQL().SELECT(mataDate.getBaseColumns()) .FROM(mataDate.getTableName()) .WHERE(getEquals(column, property)) .toString(); } public String selectByCondition(ProviderContext context, Object condition) { Class<?> entityClass = getEntityClass(context); TableMataDate mataDate = TableMataDate.forClass(entityClass); Map<String, String> fieldColumnMap = mataDate.getFieldColumnMap(); SQL sql = new SQL().SELECT(mataDate.getBaseColumns()).FROM(mataDate.getTableName()); Field[] fields = condition.getClass().getDeclaredFields(); for (Field field : fields) { Condition logicCondition = field.getAnnotation(Condition.class); String mappedProperty = logicCondition == null || StringUtils.isEmpty(logicCondition.property()) ? field.getName() : logicCondition.property(); PropertyDescriptor entityPd = BeanUtils.getPropertyDescriptor(entityClass, mappedProperty); if (entityPd == null) { continue; } PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(condition.getClass(), field.getName()); if (pd == null || pd.getReadMethod() == null) { continue; } String column = fieldColumnMap.get(mappedProperty); Object value = ReflectionUtils.invokeMethod(pd.getReadMethod(), condition); if (!StringUtils.isEmpty(value)) { Logic logic = logicCondition == null ? Logic.EQ : logicCondition.logic(); if (logic == Logic.IN || logic == Logic.NOT_IN) { if (value instanceof Collection) { sql.WHERE(column + logic.getCode() + inExpression(field.getName(), ((Collection) value).size())); } } else if (logic == Logic.NULL || logic == Logic.NOT_NULL) { sql.WHERE(column + logic.getCode()); } else { sql.WHERE(column + logic.getCode() + getTokenParam(mappedProperty)); } } } return sql.toString(); } private Class<?> getEntityClass(ProviderContext context) { Class<?> mapperType = context.getMapperType(); for (Type parent : mapperType.getGenericInterfaces()) { ResolvableType parentType = ResolvableType.forType(parent); if (parentType.getRawClass() == BaseMapper.class) { return parentType.getGeneric(0).getRawClass(); } } return null; } private String getEquals(String column, String property) { return column + " = " + getTokenParam(property); } private String getTokenParam(String property) { return "#{" + property + "}"; } private String inExpression(String property, int size) { MessageFormat messageFormat = new MessageFormat("#'{'" + property + "[{0}]}"); StringBuilder sb = new StringBuilder(" ("); for (int i = 0; i < size; i++) { sb.append(messageFormat.format(new Object[]{i})); if (i != size - 1) { sb.append(", "); } } return sb.append(")").toString(); } }
@Getter public class TableMataDate { private static final Map<Class<?>, TableMataDate> TABLE_CACHE = new ConcurrentHashMap<>(64); /** * 表名 */ private String tableName; /** * 主鍵屬性名 */ private String pkProperty; /** * 主鍵對應的列名 */ private String pkColumn; /** * 屬性名和字段名映射關係的 map */ private Map<String, String> fieldColumnMap; /** * 字段類型 */ private Map<String, Class<?>> fieldTypeMap; private TableMataDate(Class<?> clazz) { fieldColumnMap = new HashMap<>(); fieldTypeMap = new HashMap<>(); initTableInfo(clazz); } public static TableMataDate forClass(Class<?> entityClass) { TableMataDate tableMataDate = TABLE_CACHE.get(entityClass); if (tableMataDate == null) { tableMataDate = new TableMataDate(entityClass); TABLE_CACHE.put(entityClass, tableMataDate); } return tableMataDate; } public String getBaseColumns() { Collection<String> columns = fieldColumnMap.values(); if (CollectionUtils.isEmpty(columns)) { return ""; } Iterator<String> iterator = columns.iterator(); StringBuilder sb = new StringBuilder(); while (iterator.hasNext()) { String next = iterator.next(); sb.append(tableName).append(".").append(next); if (iterator.hasNext()) { sb.append(", "); } } return sb.toString(); } /** * 根據註解初始化表信息, * * @param clazz 實體類的 class */ private void initTableInfo(Class<?> clazz) { tableName = clazz.isAnnotationPresent(Table.class) ? clazz.getAnnotation(Table.class).name() : NameUtils.getUnderLineName(clazz.getSimpleName()); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { // 過濾靜態字段和有 @Transient 註解的字段 if (Modifier.isStatic(field.getModifiers()) || field.isAnnotationPresent(Transient.class) || !BeanUtils.isSimpleValueType(field.getType())) { continue; } String property = field.getName(); Column column = field.getAnnotation(Column.class); String columnName = column != null ? column.name().toLowerCase() : NameUtils.getUnderLineName(property); // 主鍵信息 : 有 @Id 註解的字段,沒有默認是 類名+Id if (field.isAnnotationPresent(Id.class) || (property.equalsIgnoreCase("id") && pkProperty == null)) { pkProperty = property; pkColumn = columnName; } // 將字段對應的列放到 map 中 PropertyDescriptor descriptor = BeanUtils.getPropertyDescriptor(clazz, property); if (descriptor != null && descriptor.getReadMethod() != null && descriptor.getWriteMethod() != null) { fieldColumnMap.put(property, columnName); fieldTypeMap.put(property, field.getType()); } } } }
在上面生成動態sql的時候在實體上能夠加 @Column, @Table, @Id 註解保證生成的sql沒問題
可是 對於查來講, 查出來後還要轉成實體類的, 若是屬性不對應, 轉出來的實體就會缺乏值, mybatis還提供類@Results註解寫在方法上, 來自定義實體屬性和數據庫字段的映射, mybatis
可是都已經在實體類上寫上@Column表示映射關係來,再在方法上寫註解,很不雅觀,
因此咱們須要動態生成ResultMap
app
mybatis裏的接口方法 最終都會生成 MappedStaement 與之對應, 數據庫字段和實體屬性映射的信息也是保存在這裏的, 因此只須要修改 MappedStaement 裏的信息就能夠了,ide
MappedStaement能夠經過mybatis 自帶的攔截機制, 攔截 Executor 的 query 方法獲取工具
代碼以下 已整理上傳github:
@Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), }) public class ResultMapInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { if (!(invocation.getTarget() instanceof Executor)) { return invocation.proceed(); } MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; // xml sql 不作處理 if (ms.getResource().contains(".xml")) { return invocation.proceed(); } ResultMap resultMap = ms.getResultMaps().iterator().next(); if (!CollectionUtils.isEmpty(resultMap.getResultMappings())) { return invocation.proceed(); } Class<?> mapType = resultMap.getType(); if (ClassUtils.isAssignable(mapType, Collection.class)) { return invocation.proceed(); } TableMataDate mataDate = TableMataDate.forClass(mapType); Map<String, Class<?>> fieldTypeMap = mataDate.getFieldTypeMap(); // List<ResultMapping> resultMappings = new ArrayList<>(fieldTypeMap.size()); for (Map.Entry<String, String> entry : mataDate.getFieldColumnMap().entrySet()) { ResultMapping resultMapping = new ResultMapping.Builder(ms.getConfiguration(), entry.getKey(), entry.getValue(), fieldTypeMap.get(entry.getKey())).build(); resultMappings.add(resultMapping); } ResultMap newRm = new ResultMap.Builder(ms.getConfiguration(), resultMap.getId(), mapType, resultMappings).build(); Field field = ReflectionUtils.findField(MappedStatement.class, "resultMaps"); ReflectionUtils.makeAccessible(field); ReflectionUtils.setField(field, ms, Collections.singletonList(newRm)); return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
新建 GoodsMapper 繼承 BaseMapper
public interface GoodsMapper extends BaseMapper<Goods> { }
@Data public class Goods implements Serializable { private static final long serialVersionUID = -6305173237589282633L; private Long id; private String code; private String fullName; private Double price; private Date createdAt; }
根據實體某個屬性查詢, eg: 根據商品 code 查詢一條商品記錄:
@Test public void test4() { Goods goods = goodsMapper.selectByProperty(Goods::getCode, "2332"); }
根據查詢條件查詢*
新建商品的查詢條件 GoodCondition
@Data public class GoodsCondition implements Serializable { private static final long serialVersionUID = -1113673119261537637L; private Long id; // @Condition(logic = Logic.IN, property = "code") private List<String> codes; private Double price; // @Condition(logic = Logic.LIKE) private String fullName; private String code; }
使用 通用mapper的 selectByCondition 方法查詢
@Test public void test3() { GoodsCondition condition = new GoodsCondition(); condition.setId(2L); condition.setCodes(Arrays.asList("12", "13")); condition.setFullName("2312312"); condition.setPrice(12.3); goodsMapper.selectByCondition(condition); }
默認是以實體中存在的屬性且值不爲空做爲查詢條件, 默認是 = 條件,
因此condition 中 codes 雖然有值, 可是實體中沒這個屬性, 因此不做爲查詢條件,
能夠加 @Condition 註解改變默認條件 和匹配的實體屬性
,將上面的註釋打開,再次執行
能夠看到 註解生效
本文代碼已整理上傳github
以爲有用的同窗 給個 star 啊 ……^_^