preface
The jPA used by the company before feels very convenient, but the new project chooses to use Mybatis. SQL is written in XML files. Although basic methods are generated by tools, it is really uncomfortable to modify these methods once a field is added to the data, and I feel really tired when I read XML files. For a while, while the project is idle, explore how to discard XML files, use annotations entirely, and pull out common methods into a base class.
The code has been uploaded to Github
How to implement BaseMapper<T>
A universal mapper generally contains basic additions, deletions, and changes, by ID, by an attribute, and by a set of conditions. When used by direct inheritance, generics are concrete entity classes. Mybatis provides @insertProvider, @selectProvider, etc. to dynamically generate SQL, so the general mapper uses these annotations.
General mapper dynamic generation OF SQL ideas is to get the entity class, according to the class to parse out the corresponding table metadata, including table name, primary key information, database fields, etc., in accordance with these information dynamic generation SQL.
For inserts and updates, the method arguments are entity objects, which can be obtained by getClass(). For queries and deletes, the method arguments are not entity objects. Since version 3.4.5, we can pass an additional parameter — ProviderContext when calling provider methods. The ProviderContext then gets the class and method of the current mapper.
Mybatis version is 3.4.6, which relies on some spring utility classes
BaseMapper<Entity>.java
public interface BaseMapper<Entity> {
/** * Add a new record **@paramThe entity entities *@returnAffected records */
@InsertProvider(type = BaseSqlProvider.class, method = "insert")
@Options(useGeneratedKeys = true, keyColumn = "id")
int insert(Entity entity);
/** * Update a record **@param entity entity
* @returnAffected records */
@UpdateProvider(type = BaseSqlProvider.class, method = "update")
int update(Entity entity);
/** * Delete a record **@param id id
* @returnAffected records */
@DeleteProvider(type = BaseSqlProvider.class, method = "delete")
int delete(Long id);
/** * query ** by id@param id id
* @return Entity
*/
@SelectProvider(type = BaseSqlProvider.class, method = "selectById")
Entity selectById(Long id);
/** * Query a record by attribute **@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);
/** * Query the list of records by attribute **@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);
/** ** Query records according to query criteria **@param condition condition
* @param <Condition> Condition
* @return List Entity
*/
@SelectProvider(type = BaseSqlProvider.class, method = "selectByCondition")
<Condition> List<Entity> selectByCondition(Condition condition);
}
Copy the code
BaseSqlProvider.java
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()) {
// Ignore the primary key
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()) {
// Ignore the primary key
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 instanceofCollection) { 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();
}
privateClass<? > 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 + "[j] {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(); }}Copy the code
Some other classes
@Getter
public class TableMataDate {
private static finalMap<Class<? >, TableMataDate> TABLE_CACHE =new ConcurrentHashMap<>(64);
/** * table name */
private String tableName;
/** * Primary key attribute name */
private String pkProperty;
/** * the column name corresponding to the primary key */
private String pkColumn;
/** * Map of attribute and field names */
private Map<String, String> fieldColumnMap;
/** * Field type */
privateMap<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(a) {
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();
}
/** * Initializes table information based on annotations, **@paramClazz entity 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) {
// Filter static fields and @TRANSIENT fields
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);
// Primary key information: @id annotated field, no default class name +Id
if (field.isAnnotationPresent(Id.class) || (property.equalsIgnoreCase("id") && pkProperty == null)) {
pkProperty = property;
pkColumn = columnName;
}
// Put the corresponding column of the field into the 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()); }}}}Copy the code
Database field and entity attribute inconsistent also underline hump how to do
You can add @column, @table, @ID annotation to the entity to make sure that the SQL generated is ok, but for lookup, you need to convert it to the entity class, if the attributes don’t correspond, the entity generated will be missing values, Mybatis also provides class @Results annotations written on methods to define mappings between entity attributes and database fields,
However, it is unsightly to write annotations on methods after putting @Column on the entity class to represent the mapping, so we need to dynamically generate ResultMap
Mybatis interface methods will eventually generate the corresponding MappedStaement, database field and entity attribute mapping information is also stored here, so you only need to modify the information in MappedStaement.
MappedStaement can be obtained by intercepting the Executor query method using mybatis’s built-in interception mechanism
The following code has been collated and uploaded to 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 is not processed
if (ms.getResource().contains(".xml")) {
return invocation.proceed();
}
ResultMap resultMap = ms.getResultMaps().iterator().next();
if(! CollectionUtils.isEmpty(resultMap.getResultMappings())) {returninvocation.proceed(); } Class<? > mapType = resultMap.getType();if (ClassUtils.isAssignable(mapType, Collection.class)) {
returninvocation.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) {}}Copy the code
Use effect
Create GoodsMapper to inherit BaseMapper
public interface GoodsMapper extends BaseMapper<Goods> {}Copy the code
@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;
}
Copy the code
The query
Query according to an entity attribute, eg: Query a commodity record according to the commodity code:
@Test
public void test4(a) {
Goods goods = goodsMapper.selectByProperty(Goods::getCode, "2332");
}
Copy the code
Query * according to the query condition
Query condition GoodCondition of a new commodity
@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;
}
Copy the code
Use the selectByCondition method of the universal Mapper
@Test
public void test3(a) {
GoodsCondition condition = new GoodsCondition();
condition.setId(2L);
condition.setCodes(Arrays.asList("12"."13"));
condition.setFullName("2312312");
condition.setPrice(12.3);
goodsMapper.selectByCondition(condition);
}
Copy the code
By default, the attribute that exists in the entity and its value is not empty is used as the query condition. By default, it is = condition. Therefore, although codes in condition have values, they do not exist in the entity, so they are not used as the query condition. You can change the default Condition and matching entity attributes with the @condition annotation, open the comment above, and execute again
You can see the annotations in effect
The code has been uploaded to Github
Give a star to those who think it is useful… ^_^