詳解如何開(kāi)發(fā)一個(gè)MyBatis通用Mapper的輪子
一、前言
程序猿為什么如此執(zhí)著于造輪子?MyBatis-Plus如此強(qiáng)大的工具流行這么多年了,我為啥還在重復(fù)造這樣的輪子?
1、公司的技術(shù)規(guī)范不允許使用MyBatis-Plus,咱也不知道什么原因;
3、以前使用SpringDataJpa慣了,今年第一次用MyBatis,必須把它打造成我想要的樣子;
6、MyBatis-Plus好像不支持聯(lián)合主鍵;
7、還有一些其它的需求,比如對(duì)字典字段自動(dòng)翻譯:字典可能來(lái)自枚舉、字典表、Redis......
10、通用數(shù)據(jù)權(quán)限控制;
11、如果不造此輪子,就沒(méi)有這篇文章。
以上12點(diǎn)原因,便是造這個(gè)輪子的理由。實(shí)際上,輪子不重要,重要的是掌握輪子的原理,取其精華,去其糟粕。也歡迎大家拍磚,請(qǐng)輕拍,數(shù)學(xué)能力被誰(shuí)拍壞了誰(shuí)來(lái)陪。
二、需求
通用Mapper起碼應(yīng)該包含以下功能:
1、增
2、刪
3、改
4、批量增
5、批量刪
6、只更新指定字段
7、分頁(yè)查詢查當(dāng)前頁(yè)
8、分頁(yè)查詢查總數(shù)
9、字典字段翻譯
10、數(shù)據(jù)權(quán)限控制
大概長(zhǎng)下面這個(gè)樣子:
public interface BaseMapper<T,K> { int insert(T t); int batchInsert(List<T> entity); int deleteById(K id); int deleteBatchIds(Collection<K> ids); int updateById(T entity); int updateSelectiveById(T entity); T selectById(K id); List<T> selectBatchIds(Collection<K> ids); List<T> selectAll(); List<T> selectPage(PageRequest<T> pageRequest); Long selectCount(T entity); }
三、實(shí)現(xiàn)原理
1、基于MyBatis3提供的SqlProvider構(gòu)建動(dòng)態(tài)Sql
例如如下代碼:
@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName") List<User> getUsersByName(String name); class UserSqlBuilder { public static String buildGetUsersByName(final String name) { return new SQL(){{ SELECT("*"); FROM("users"); if (name != null) { WHERE("name like #{value} || '%'"); } ORDER_BY("id"); }}.toString(); } }
2、基于自定義注解,為實(shí)體和數(shù)據(jù)庫(kù)表建立對(duì)應(yīng)關(guān)系
例如如下代碼:
@Table("user") public class User { @Id(auto = true) @Column(value = "id") private Long id; @Column(value = "name", filterOperator = FilterOperator.LIKE) @OrderBy(orderPriority = 0) private String name; @OrderBy(order = Order.DESC, orderPriority = 1) private Integer age; private String email; @Transient private String test; }
基于以上兩個(gè)原理,當(dāng)方法被調(diào)用時(shí),我們便可構(gòu)建出相應(yīng)的動(dòng)態(tài)Sql,從而實(shí)現(xiàn)該通用Mapper。
四、代碼實(shí)現(xiàn)
1、自定義注解
1)@Table
了解Jpa的朋友一定很熟悉,這個(gè)就是為實(shí)體指定表名,實(shí)體不加這個(gè)注解就認(rèn)為實(shí)體名與表名一致:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Table { //表名,不指定則使用實(shí)體類名 String value() default ""; }
2)@Column
指定完表名,該指定列名了,同樣的如果字段不指定則認(rèn)為字段名與表列名一致:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Column { //對(duì)應(yīng)數(shù)據(jù)庫(kù)列名 String value() default ""; //查詢時(shí)的過(guò)濾類型 FilterOperator filterOperator() default FilterOperator.EQ; //是否查詢,select是否帶上該字段 boolean selectable() default true; //是否插入,insert是否帶上該字段 boolean insertable() default true; //是否更新,update是否帶上該字段 boolean updatable() default true; }
3)@Id
這個(gè)注解就是為了表明該字段是否是數(shù)據(jù)庫(kù)主鍵。當(dāng)然,這個(gè)注解可以與@Column合并,但為了更清晰,我還是決定單獨(dú)使用這個(gè)注解。并且,也方便后期擴(kuò)展。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Id { //主鍵是否自動(dòng)生成 boolean auto() default false; }
4)@OrderBy
這個(gè)注解來(lái)標(biāo)明查詢時(shí)的排序字段,同時(shí)考慮如果排序字段有多個(gè),可定義優(yōu)先級(jí):
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface OrderBy { //排序 Order order() default Order.ASC; //多個(gè)排序字段先后順序 int orderPriority() default 0; }
5)@Transient
考慮實(shí)體中有些字段在數(shù)據(jù)庫(kù)中不存在的情況。使用這個(gè)注解來(lái)標(biāo)注這樣的字段:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Transient { }
2、幾個(gè)pojo,用來(lái)保存實(shí)體對(duì)應(yīng)的信息
1)TableInfo,表示一個(gè)實(shí)體對(duì)應(yīng)的數(shù)據(jù)庫(kù)表信息
public class TableInfo { //表對(duì)應(yīng)的實(shí)體類型 private Class<?> entityClass; //表名 private String tableName; //列 private List<ColumnInfo> columns; //是否聯(lián)合主鍵 private boolean isUnionId; }
2)ColumnInfo,表示實(shí)體中的一個(gè)字段對(duì)應(yīng)的數(shù)據(jù)庫(kù)表字段信息
public class ColumnInfo { //對(duì)應(yīng)的java類型 private Class<?> fieldClass; private Field field; private FilterOperator filterOperator; //數(shù)據(jù)庫(kù)列 private String column; //是否主鍵 private boolean isPrimaryKey; //主鍵填充方式 private boolean isPrimaryKeyAuto; //排序 private Order orderBy; private int orderByPriority; //是否參與insert private boolean insertable; //是否參與update private boolean updatable; //是否參與select private boolean selectable; }
以上只需要注意一點(diǎn),如何判斷一個(gè)實(shí)體是否是聯(lián)合主鍵。這里用的比較粗暴的方法,如果有多個(gè)字段加了@Id,那么認(rèn)為是聯(lián)合主鍵。
3、定義開(kāi)頭說(shuō)的BaseMapper
這個(gè)BaseMapper的定義模仿了SpringDataJpa,它需要兩個(gè)泛型,T表示實(shí)體類型,K表示主鍵類型。
一般情況下K為簡(jiǎn)單數(shù)據(jù)類型,比如Long,String;
聯(lián)合主鍵情況下,K為自定義的一個(gè)復(fù)雜數(shù)據(jù)類型,具體使用方法見(jiàn)文章最后章節(jié)。
public interface BaseMapper<T,K> { @InsertProvider(type = SqlProvider.class,method = "insert") @Options(useGeneratedKeys = true, keyProperty = "id",keyColumn = "id") int insert(T t); @InsertProvider(type = SqlProvider.class,method = "batchInsert") int batchInsert(@Param("list") List<T> entity); @DeleteProvider(type = SqlProvider.class,method = "deleteById") int deleteById(@Param("id") K id); @DeleteProvider(type = SqlProvider.class,method = "deleteBatchIds") int deleteBatchIds(@Param("ids") Collection<K> ids); @UpdateProvider(type = SqlProvider.class,method = "updateById") int updateById(T entity); @UpdateProvider(type = SqlProvider.class,method = "updateSelectiveById") int updateSelectiveById(T entity); @SelectProvider(type = SqlProvider.class,method = "selectById") T selectById(@Param("id") K id); @SelectProvider(type = SqlProvider.class,method = "selectBatchIds") List<T> selectBatchIds(@Param("ids") Collection<K> ids); @SelectProvider(type = SqlProvider.class,method = "selectAll") List<T> selectAll(); @SelectProvider(type = SqlProvider.class,method = "selectPage") List<T> selectPage(PageRequest<T> pageRequest); @SelectProvider(type = SqlProvider.class,method = "selectCount") Long selectCount(T entity); }
4、SqlProvider
public class SqlProvider<T> { private static Logger logger = LoggerFactory.getLogger(SqlProvider.class); private static Map<Class<?>, TableInfo> tableCache = new ConcurrentHashMap<>(); public String insert(T entity, ProviderContext context){ TableInfo tableInfo = getTableInfo(context); String tableName = tableInfo.getTableName(); String intoColumns = tableInfo.getColumns() .stream() .filter(ColumnInfo::isInsertable) .map(ColumnInfo::getColumn) .collect(Collectors.joining(",")); String values = tableInfo.getColumns() .stream() .filter(ColumnInfo::isInsertable) .map(ColumnInfo::variable) .collect(Collectors.joining(",")); String sql = new SQL() .INSERT_INTO(tableName) .INTO_COLUMNS(intoColumns) .INTO_VALUES(values).toString(); logger.info("sql->{},params->{}",sql,entity); return sql; } public String batchInsert(@Param("list" ) List<?> entity, ProviderContext context){ TableInfo tableInfo = getTableInfo(context); String tableName = tableInfo.getTableName(); String intoColumns = tableInfo.getColumns() .stream() .filter(ColumnInfo::isInsertable) .map(ColumnInfo::getColumn) .collect(Collectors.joining(",")); String values = tableInfo.getColumns() .stream() .filter(ColumnInfo::isInsertable) .map(column->column.variableWithPrefix("item")) .collect(Collectors.joining(",")); String sql = new SQL() .INSERT_INTO(tableName) .INTO_COLUMNS(intoColumns).toString(); sql += " values "; sql += "<foreach collection=\"list\" item=\"item\" separator=\",\">" + " <trim prefix=\"(\" suffix=\")\" suffixOverrides=\",\">" + " " + values + " </trim>" + "</foreach>"; sql = "<script>"+sql+"</script>"; logger.info("sql->{},params->{}",sql,entity); return sql; } public String deleteById(@Param("id") T entity, ProviderContext context){ TableInfo tableInfo = getTableInfo(context); String tableName = tableInfo.getTableName(); String[] where = null; if (tableInfo.isUnionId()){ where = tableInfo.getColumns() .stream() .filter(ColumnInfo::isPrimaryKey) .map(columnInfo -> columnInfo.getColumn()+" = #{id."+columnInfo.getField().getName()+"}") .toArray(String[]::new); }else { where = tableInfo.getColumns() .stream() .filter(ColumnInfo::isPrimaryKey) .map(columnInfo -> columnInfo.getColumn()+" = #{id}") .toArray(String[]::new); } String sql = new SQL() .DELETE_FROM(tableName) .WHERE(where) .toString(); logger.info("sql->{},params->{}",sql,entity); return sql; } public String deleteBatchIds(@Param("ids") Collection<?> entity, ProviderContext context){ TableInfo tableInfo = getTableInfo(context); String tableName = tableInfo.getTableName(); if (tableInfo.isUnionId()){ String[] where = new String[entity.size()]; for (int i = 0; i < entity.size(); i++){ List<String> list = new ArrayList<>(); String s = "%s=#{ids[%d].%s}"; for (ColumnInfo columnInfo:tableInfo.getColumns()){ if (columnInfo.isPrimaryKey()){ list.add(String.format(s,columnInfo.getColumn(),i,columnInfo.getField().getName())); } } where[i] = "("+StringUtils.join(list," and ")+")"; } String sql = "delete from %s where %s "; sql = String.format(sql,tableName,StringUtils.join(where," or ")); logger.info("sql->{},params->{}",sql,entity); return sql; }else { String idName = tableInfo.getColumns() .stream() .filter(ColumnInfo::isPrimaryKey) .findFirst() .get() .getColumn(); String sql = "DELETE FROM %s WHERE %s IN (%s) "; String[] arr = new String[entity.size()]; for (int i = 0; i < entity.size(); i++){ arr[i] = "#{ids["+i+"]}"; } sql = String.format(sql,tableName,idName,StringUtils.join(arr,",")); logger.info("sql->{},params->{}",sql,entity); return sql; } } public String updateById(T entity, ProviderContext context){ TableInfo tableInfo = getTableInfo(context); String tableName = tableInfo.getTableName(); String[] where = tableInfo.getColumns() .stream() .filter(ColumnInfo::isPrimaryKey) .map(columnInfo -> columnInfo.getColumn()+" = "+columnInfo.variable()) .toArray(String[]::new); String sql = new SQL().UPDATE(tableName).SET(tableInfo.updateSetColumn()).WHERE(where).toString(); logger.info("sql->{},params->{}",sql,entity); return sql; } public String updateSelectiveById(T entity, ProviderContext context){ TableInfo tableInfo = getTableInfo(context); String tableName = tableInfo.getTableName(); String[] where = tableInfo.getColumns() .stream() .filter(ColumnInfo::isPrimaryKey) .map(columnInfo -> columnInfo.getColumn()+" = "+columnInfo.variable()) .toArray(String[]::new); String sql = new SQL().UPDATE(tableName).SET(tableInfo.updateSetSelectiveColumn(entity)).WHERE(where).toString(); logger.info("sql->{},params->{}",sql,entity); return sql; } public String selectById(@Param("id")T entity, ProviderContext context){ TableInfo tableInfo = getTableInfo(context); String[] where = null; if (tableInfo.isUnionId()){ where = tableInfo.getColumns().stream().filter(ColumnInfo::isPrimaryKey) .map(columnInfo -> columnInfo.getColumn()+" = #{id."+columnInfo.getField().getName()+"}") .toArray(String[]::new); }else { where = tableInfo.getColumns().stream().filter(ColumnInfo::isPrimaryKey) .map(columnInfo -> columnInfo.getColumn()+" = #{id}") .toArray(String[]::new); } String sql = new SQL() .SELECT(tableInfo.selectColumnAsProperty()) .FROM(tableInfo.getTableName()) .WHERE(where) .toString(); logger.info("sql->{},params->{}",sql,entity); return sql; } public String selectBatchIds(@Param("ids")Collection<?> entity, ProviderContext context){ TableInfo tableInfo = getTableInfo(context); String tableName = tableInfo.getTableName(); if (tableInfo.isUnionId()){ String[] where = new String[entity.size()]; for (int i = 0; i < entity.size(); i++){ List<String> list = new ArrayList<>(); String s = "%s=#{ids[%d].%s}"; for (ColumnInfo columnInfo:tableInfo.getColumns()){ if (columnInfo.isPrimaryKey()){ list.add(String.format(s,columnInfo.getColumn(),i,columnInfo.getField().getName())); } } where[i] = "("+StringUtils.join(list," and ")+")"; } String sql = "select %s from %s where %s"; sql = String.format(sql,tableInfo.selectColumnAsProperty(),tableInfo.getTableName(),StringUtils.join(where," or ")); logger.info("sql->{},params->{}",sql,entity); return sql; }else { String idName = tableInfo.getColumns() .stream() .filter(ColumnInfo::isPrimaryKey) .findFirst() .get() .getColumn(); String sql = "select %s from %s where %s in (%s) "; String[] arr = new String[entity.size()]; for (int i = 0; i < entity.size(); i++){ arr[i] = "#{ids["+i+"]}"; } sql = String.format(sql,tableInfo.selectColumnAsProperty(),tableName,idName,StringUtils.join(arr,",")); logger.info("sql->{},params->{}",sql,entity); return sql; } } public String selectAll(T entity, ProviderContext context){ TableInfo tableInfo = getTableInfo(context); SQL sql = new SQL() .SELECT(tableInfo.selectColumnAsProperty()) .FROM(tableInfo.getTableName()); String orderBy = tableInfo.orderByColumn(); if (StringUtils.isNotEmpty(orderBy)){ sql.ORDER_BY(orderBy); } return sql.toString(); } public String selectPage(PageRequest<T> entity, ProviderContext context){ TableInfo tableInfo = getTableInfo(context); SQL sql = new SQL() .SELECT(tableInfo.selectColumnAsProperty()) .FROM(tableInfo.getTableName()); String[] where = tableInfo.getColumns().stream() .filter(column -> { Field field = column.getField(); T bean = entity.getPageParams(); Object value = Util.getFieldValue(bean, field); if (value == null) { return false; } return StringUtils.isNotEmpty(value.toString()); }) .map(column -> { String param = " #{pageParams." + column.getField().getName()+"}"; if (column.getFilterOperator() == FilterOperator.LIKE){ param = "concat('%', "+param+", '%')"; } if (column.getFilterOperator() == FilterOperator.LEFTLIKE){ param = "concat("+param+", '%')"; } if (column.getFilterOperator() == FilterOperator.RIGHTLIKE){ param = "concat('%', "+param+")"; } return column.getColumn()+column.filterOperator()+param; }) .toArray(String[]::new); sql.WHERE(where); if (StringUtils.isNotEmpty(entity.getOrder())){ ColumnInfo columnInfo = tableInfo.getColumns().stream() .filter(columnInfo1 -> columnInfo1.getField().getName().equalsIgnoreCase(entity.getOrder())) .findFirst().orElse(null); if (columnInfo != null){ String direction = entity.getOrderDirection(); direction = (StringUtils.isEmpty(direction) || direction.equalsIgnoreCase("asc"))?" asc ":" desc "; sql.ORDER_BY(columnInfo.getColumn() + direction); } }else { String orderBy = tableInfo.orderByColumn(); if (StringUtils.isNotEmpty(orderBy)){ sql.ORDER_BY(orderBy); } } sql.OFFSET("#{offset}").LIMIT("#{pageSize}"); String s = sql.toString(); logger.info("sql->{},params->{}",s,entity); return s; } public String selectCount(T entity, ProviderContext context){ TableInfo tableInfo = getTableInfo(context); SQL sql = new SQL() .SELECT("count(1)") .FROM(tableInfo.getTableName()); String[] where = tableInfo.getColumns().stream() .filter(column -> { Field field = column.getField(); Object value = Util.getFieldValue(entity, field); if (value == null) { return false; } return StringUtils.isNotEmpty(value.toString()); }) .map(column -> { String param = " #{" + column.getField().getName()+"}"; if (column.getFilterOperator() == FilterOperator.LIKE){ param = "concat('%', "+param+", '%')"; } if (column.getFilterOperator() == FilterOperator.LEFTLIKE){ param = "concat("+param+", '%')"; } if (column.getFilterOperator() == FilterOperator.RIGHTLIKE){ param = "concat('%', "+param+")"; } return column.getColumn()+column.filterOperator()+param; }) .toArray(String[]::new); sql.WHERE(where); String s = sql.toString(); logger.info("sql->{},params->{}",s,entity); return s; } private TableInfo getTableInfo(ProviderContext context){ Class<?> clz = getEntityType(context); return tableCache.computeIfAbsent(context.getMapperType(), t-> Util.tableInfo(clz)); } private Class<?> getEntityType(ProviderContext context) { return Stream.of(context.getMapperType().getGenericInterfaces()) .filter(ParameterizedType.class::isInstance) .map(ParameterizedType.class::cast) .filter(type -> type.getRawType() == BaseMapper.class) .findFirst() .map(type -> type.getActualTypeArguments()[0]) .filter(Class.class::isInstance) .map(Class.class::cast) .orElseThrow(() -> new IllegalStateException("未找到BaseMapper的泛型類 " + context.getMapperType().getName() + ".")); } }
5、實(shí)體類轉(zhuǎn)TableInfo
public static TableInfo tableInfo(Class<?> entityClass) { TableInfo info = new TableInfo(); info.setEntityClass(entityClass); Table table = entityClass.getAnnotation(Table.class); String tableName = entityClass.getSimpleName(); if (table != null && StringUtils.isNotEmpty(table.value())){ tableName = table.value(); } info.setTableName(tableName); Field[] allFields = getFields(entityClass); Field[] fields = Stream.of(allFields) //過(guò)濾@Transient注解的field .filter(field -> !field.isAnnotationPresent(Transient.class)) .toArray(Field[]::new); List<ColumnInfo> columns = new ArrayList<>(); int idCount = 0; for (Field field:fields){ ColumnInfo columnInfo = new ColumnInfo(); columnInfo.setFieldClass(field.getDeclaringClass()); columnInfo.setField(field); Id id = field.getAnnotation(Id.class); idCount = idCount + (id == null?0:1); columnInfo.setPrimaryKey(id == null?Boolean.FALSE:Boolean.TRUE); columnInfo.setPrimaryKeyAuto(id == null?Boolean.FALSE:id.auto()); Column column = field.getAnnotation(Column.class); String columnName = field.getName(); if (column != null && StringUtils.isNotEmpty(column.value())){ columnName = column.value(); } columnInfo.setColumn(columnName); FilterOperator filterOperator = FilterOperator.EQ; if (column != null && column.filterOperator() != null){ filterOperator = column.filterOperator(); } columnInfo.setFilterOperator(filterOperator); if (columnInfo.isPrimaryKeyAuto()){ columnInfo.setInsertable(false); }else { columnInfo.setInsertable(true); if (column != null){ columnInfo.setInsertable(column.insertable()); } } columnInfo.setUpdatable(true); columnInfo.setSelectable(true); if (column != null){ columnInfo.setSelectable(column.selectable()); columnInfo.setUpdatable(column.updatable()); } OrderBy orderBy = field.getAnnotation(OrderBy.class); if (orderBy != null){ columnInfo.setOrderBy(orderBy.order()); columnInfo.setOrderByPriority(orderBy.orderPriority()); } columns.add(columnInfo); } if (idCount > 1){ info.setUnionId(Boolean.TRUE); } info.setColumns(columns); return info; }
6、字典字段自動(dòng)翻譯
簡(jiǎn)單實(shí)現(xiàn)思路:對(duì)需要翻譯的字段加上@FieldTrans注解來(lái)表明這個(gè)字段需要翻譯,通過(guò)AOP方式對(duì)結(jié)果數(shù)據(jù)進(jìn)行增強(qiáng),來(lái)將字段進(jìn)行翻譯更新。
此部分內(nèi)容留待后續(xù)實(shí)現(xiàn),同時(shí)調(diào)研一下是否還有更優(yōu)雅簡(jiǎn)單的實(shí)現(xiàn)方式。
7、數(shù)據(jù)權(quán)限
我們先來(lái)思考一下數(shù)據(jù)權(quán)限到底要干啥?一句話來(lái)概括:查一張表的數(shù)據(jù)時(shí)在where條件中追加“and 控制權(quán)限的列 in (???)”。
簡(jiǎn)單實(shí)現(xiàn)方法:在控制權(quán)限的字段加上@DataAuthrity注解來(lái)表明通過(guò)這個(gè)字段控制權(quán)限,而???的內(nèi)容肯定是由業(yè)務(wù)代碼來(lái)生成的,因此考慮給這個(gè)注解增加一個(gè)屬性,用來(lái)指明權(quán)限數(shù)據(jù)由執(zhí)行哪個(gè)接口或方法來(lái)獲取。
此部分內(nèi)容留待后續(xù)實(shí)現(xiàn),同時(shí)調(diào)研一下是否還有更優(yōu)雅簡(jiǎn)單的實(shí)現(xiàn)方式。
五、使用示例
1、數(shù)據(jù)庫(kù)表
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `age` int(11) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2、實(shí)體
@Table("user") public class User { @Id(auto = true) @Column(value = "id") private Long id; @Column(value = "name", filterOperator = FilterOperator.LIKE) @OrderBy(orderPriority = 0) private String name; @OrderBy(order = Order.DESC, orderPriority = 1) private Integer age; private String email; @Transient private String test; }
3、Mapper
public interface UserMapper extends BaseMapper<User, Long> { }
至此,不需要寫(xiě)任何mapper.xml,UserMapper已經(jīng)具備了增刪改查能力。
4、聯(lián)合主鍵示例
public class User1 { @Id @Column(value = "id1") private String id1; @Id @Column(value = "id2") private String id2; @Column(value = "name", filterOperator = FilterOperator.LIKE) @OrderBy(orderPriority = 0) private String name; @OrderBy(order = Order.DESC, orderPriority = 1) private Integer age; private String email; @Transient private String test; } public class User1Id { private String id1; private String id2; } public interface User1Mapper extends BaseMapper<User1,User1Id> { }
六、總結(jié)
本輪子目前基本上不值一提,但相信后面我再把字典翻譯、通用數(shù)據(jù)權(quán)限加上的話,仍然會(huì)不值一提。
實(shí)際上輪子本身不重要,開(kāi)發(fā)過(guò)程中的各種思考、試驗(yàn)更重要吧。
以上就是詳解如何開(kāi)發(fā)一個(gè)MyBatis通用Mapper的輪子的詳細(xì)內(nèi)容,更多關(guān)于MyBatis通用Mapper輪子的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring Boot示例分析講解自動(dòng)化裝配機(jī)制核心注解
這篇文章主要分析了Spring Boot 自動(dòng)化裝配機(jī)制核心注解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-07-07Spring?MVC文件請(qǐng)求處理MultipartResolver詳解
這篇文章主要介紹了Spring?MVC文件請(qǐng)求處理詳解:MultipartResolver,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-11-11IDEA的Terminal無(wú)法執(zhí)行g(shù)it命令問(wèn)題
這篇文章主要介紹了IDEA的Terminal無(wú)法執(zhí)行g(shù)it命令問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09Java 獲取Html文本中的img標(biāo)簽下src中的內(nèi)容方法
今天小編就為大家分享一篇Java 獲取Html文本中的img標(biāo)簽下src中的內(nèi)容方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-06-06Java中的抽象工廠模式_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
抽象工廠模式是工廠方法模式的升級(jí)版本,他用來(lái)創(chuàng)建一組相關(guān)或者相互依賴的對(duì)象。下面通過(guò)本文給大家分享Java中的抽象工廠模式,感興趣的朋友一起看看吧2017-08-08springboot新建項(xiàng)目pom.xml文件第一行報(bào)錯(cuò)的解決
這篇文章主要介紹了springboot新建項(xiàng)目pom.xml文件第一行報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01