1. Introduction
Today we continue to build our Kono Spring Boot scaffolding, the last article to the most popular ORM framework Mybatis also integrated into it. But a lot of times we want to have some generic Mapper out of the box to simplify our development. I tried to implement one myself, and I’m going to share the idea. It was written last night, carefully used for actual production development, but can be used for reference.
Gitee: gitee.com/felord/kono day03 branch
Making: github.com/NotFound403… Day03 branch
2. Source of ideas
I’ve been reading some stuff about Spring Data JDBC and found it quite good. CrudRepository is magic, as long as the ORM interface inherits it, it is automatically added to Spring IoC, and it also has some basic database operation interfaces. I was wondering if I could combine it with Mybatis.
Spring Data JDBC itself supports Mybatis. However, after I tried to integrate them, I found that there are a lot of things to do and many conventions to follow, such as the parameter context of MybatisContext and the interface name prefix, which are relatively strict conventions, and the cost of learning to use is relatively high, which is not as good as using Spring Data JDBC alone. But I still wanted universal CRUD functionality, so I started experimenting with a simple one myself.
3. Try something
There were several ideas that came to mind at first but none of them worked out. Here is also to share, sometimes failure is also very worth learning.
3.1 Mybatis plugin
The plug-in function of Mybatis is used to develop the plug-in, but it is not feasible after a long time of research. The biggest problem is the life cycle of Mapper.
The Mapper is registered in the configuration at project startup, and the corresponding SQL is also registered in the MappedStatement object. When the Mapper method is executed, the SQL is retrieved and executed by an agent that loads the corresponding MappedStatement based on the Namespace.
The plug-in life cycle starts when the MappedStatement has been registered and does not connect at all.
3.2 Code Generator
This is completely feasible, but the cost of making wheels is higher and more mature. In the actual production and development, we will find one. It costs more time and energy to make an artificial wheel, and it is unnecessary.
3.3 Simulating MappedStatement registration
Finally, find a suitable entry point to register the MappedStatement corresponding to the universal Mapper. I’ll talk more about how I did this next.
4. Spring registration mechanism for Mapper
In the early days of Spring Boot, most Mapper registrations were done this way.
<bean id="baseMapper" class="org.mybatis.spring.mapper.MapperFactoryBean" abstract="true" lazy-init="true">
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
<bean id="oneMapper" parent="baseMapper">
<property name="mapperInterface" value="my.package.MyMapperInterface" />
</bean>
<bean id="anotherMapper" parent="baseMapper">
<property name="mapperInterface" value="my.package.MyAnotherMapperInterface" />
</bean>
Copy the code
Each Mybatis Mapper is initialized and injected into the Spring IoC container via MapperFactoryBean. So this is a place where injection of a generic Mapper is possible and less invasive. So how does it work? I found it in the familiar @mapperscan. The following is an excerpt from its source code:
/**
* Specifies a custom MapperFactoryBean to return a mybatis proxy as spring bean.
*
* @return the class of {@code MapperFactoryBean}
*/
Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
Copy the code
That is, usually @Mapperscan will batch initialize and inject Spring IoC all Mapper in a particular package using MapperFactoryBean.
5. Implement universal Mapper
Now that you understand Spring’s Mapper registration mechanism, you’re ready to implement a universal Mapper.
5.1 General Mapper Interfaces
CrudMapper
contains four basic single-table operations. CrudMapper
contains four basic single-table operations.
/** * All Mapper interfaces inherit {@code CrudMapper<T, PK>}.
*
* @param<T> Entity class generic *@param<PK> Primary key generic *@author felord.cn
* @since 14 :00
*/
public interface CrudMapper<T.PK> {
int insert(T entity);
int updateById(T entity);
int deleteById(PK id);
T findById(PK id);
}
Copy the code
The rest of the logic revolves around this interface. When the concrete Mapper inherits this interface, the entity class generic T and primary key generic PK are identified. We need to take the specific type of T and encapsulate its member properties into SQL and customize the MappedStatement.
5.2 Metadata parsing and encapsulation of Mapper
To simplify the code, entity classes make some common conventions:
- The underline style of the entity class name is the corresponding table name, for example
UserInfo
The database table name of theuser_info
. - The underline style of the entity class attribute is the field name of the corresponding database table. In addition, all attributes in the entity have corresponding database fields, which can be ignored.
- If the corresponding SQL exists for mapper. XML, this configuration is ignored.
Since the primary key attribute must be explicitly identified to obtain it, a primary key tag annotation is declared:
/**
* Demarcates an identifier.
*
* @author felord.cn
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
public @interface PrimaryKey {
}
Copy the code
Then we declare a database entity like this:
/ * * *@author felord.cn
* @since15:43 * * /
@Data
public class UserInfo implements Serializable {
private static final long serialVersionUID = -8938650956516110149L;
@PrimaryKey
private Long userId;
private String name;
private Integer age;
}
Copy the code
Then you can write a working Mapper like this.
public interface UserInfoMapper extends CrudMapper<UserInfo.String> {}
Copy the code
The next step is to encapsulate a utility class CrudMapperProvider that parses this interface. What it does is parse the UserInfoMapper Mapper and encapsulate the MappedStatement. To make it easier to understand, I illustrated the process of parsing a Mapper with examples.
public CrudMapperProvider(Class
> mapperInterface) {
// Get the specific Mapper interface such as UserInfoMapper
this.mapperInterface = mapperInterface;
Type[] genericInterfaces = mapperInterface.getGenericInterfaces();
CrudMapper
,string>
Type mapperGenericInterface = genericInterfaces[0];
// Parameterize the type
ParameterizedType genericType = (ParameterizedType) mapperGenericInterface;
// The purpose of the parameterized type is to understand the precipitation [UserInfo,String]
Type[] actualTypeArguments = genericType.getActualTypeArguments();
// Get the entity type UserInfo
this.entityType = (Class<? >) actualTypeArguments[0];
// Get the primary key type String
this.primaryKeyType = (Class<? >) actualTypeArguments[1];
// Fetching all entity-class attributes was intended to be introspective
Field[] declaredFields = this.entityType.getDeclaredFields();
// Resolve the primary key
this.identifer = Stream.of(declaredFields)
.filter(field -> field.isAnnotationPresent(PrimaryKey.class))
.findAny()
.map(Field::getName)
.orElseThrow(() -> new IllegalArgumentException(String.format("no @PrimaryKey found in %s".this.entityType.getName())));
// Parse the attribute name and encapsulate it as an underscore field to exclude static attributes. Otherwise, you can declare an ignore annotation to ignore the field if necessary
this.columnFields = Stream.of(declaredFields) .filter(field -> ! Modifier.isStatic(field.getModifiers())) .collect(Collectors.toList());// Parse the table name
this.table = camelCaseToMapUnderscore(entityType.getSimpleName()).replaceFirst("_"."");
}
Copy the code
Once you have that metadata, you generate four types of SQL. The SQL we expect, for example UserInfoMapper, looks like this:
# findById
SELECT user_id, name, age FROM user_info WHERE (user_id = #{userId})
# insert
INSERT INTO user_info (user_id, name, age) VALUES (#{userId}, #{name}, #{age})
# deleteById
DELETE FROM user_info WHERE (user_id = #{userId})
# updateById
UPDATE user_info SET name = #{name}, age = #{age} WHERE (user_id = #{userId})
Copy the code
Mybatis provides a good SQL utility class to generate these SQL:
String findSQL = new SQL()
.SELECT(COLUMNS)
.FROM(table)
.WHERE(CONDITION)
.toString();
String insertSQL = new SQL()
.INSERT_INTO(table)
.INTO_COLUMNS(COLUMNS)
.INTO_VALUES(VALUES)
.toString();
String deleteSQL = new SQL()
.DELETE_FROM(table)
.WHERE(CONDITION).toString();
String updateSQL = new SQL().UPDATE(table)
.SET(SETS)
.WHERE(CONDITION).toString();
Copy the code
We just need to use the metadata obtained by reflection to achieve dynamic creation of SQL. Take the insert method as an example:
/**
* Insert.
*
* @param configuration the configuration
*/
private void insert(Configuration configuration) {
String insertId = mapperInterface.getName().concat(".").concat("insert");
// In XML configuration, already registered skip XML has the highest priority
if (existStatement(configuration,insertId)){
return;
}
// Generate a list of database fields
String[] COLUMNS = columnFields.stream()
.map(Field::getName)
.map(CrudMapperProvider::camelCaseToMapUnderscore)
.toArray(String[]::new);
// The corresponding value is wrapped in #{}
String[] VALUES = columnFields.stream()
.map(Field::getName)
.map(name -> String.format("#{%s}", name))
.toArray(String[]::new);
String insertSQL = new SQL()
.INSERT_INTO(table)
.INTO_COLUMNS(COLUMNS)
.INTO_VALUES(VALUES)
.toString();
Map<String, Object> additionalParameters = new HashMap<>();
/ / register
doAddMappedStatement(configuration, insertId, insertSQL, SqlCommandType.INSERT, entityType, additionalParameters);
}
Copy the code
Another important thing here is that every MappedStatement has a globally unique identifier. Mybatis defaults to punctuation for fully qualified names of Mapper. The corresponding method name on the concatenation. For example, cn. Felord. Kono. MapperClientUserRoleMapper. FindById. It’s then time to define your own MapperFactoryBean.
5.3 Customizing MapperFactoryBean
A good starting point is to register MappedStatement after Mapper is registered. We can inherit MapperFactoryBean and override its checkDaoConfig method to register MappedStatement using CrudMapperProvider.
@Override
protected void checkDaoConfig(a) {
notNull(super.getSqlSessionTemplate(), "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
Class<T> mapperInterface = super.getMapperInterface();
notNull(mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (isAddToConfig()) {
try {
// Check whether Mapper is registered
if(! configuration.hasMapper(mapperInterface)) { configuration.addMapper(mapperInterface); }// Only if you inherit CrudMapper
if (CrudMapper.class.isAssignableFrom(mapperInterface)) {
// An opportunity to register an SQL map
CrudMapperProvider crudMapperProvider = new CrudMapperProvider(mapperInterface);
/ / register MappedStatementcrudMapperProvider.addMappedStatements(configuration); }}catch (Exception e) {
logger.error("Error while adding the mapper '" + mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally{ ErrorContext.instance().reset(); }}}Copy the code
5.4 Enabling the Universal Mapper
Since we override the default MapperFactoryBean, we explicitly declare the custom MybatisMapperFactoryBean enabled, as follows:
@MapperScan(basePackages = {"cn.felord.kono.mapper"},factoryBean = MybatisMapperFactoryBean.class)
Copy the code
Then a universal Mapper feature is implemented.
5.5 Project Location
This is just a small attempt of my own, I have taken out this function separately, interested in their own reference research.
- Making: github.com/NotFound403…
- Gitee: gitee.com/felord/myba…
6. Summary
The key to success is to control the life cycle of some concepts in Mybatis. In fact, most frameworks follow this idea when they need to be modified: figure out the process and find an appropriate entry point to embed custom logic. This DEMO will not incorporate the main branch, as this is just a trial run and not yet practical, you can choose other well-known frameworks to do this. Pay attention and support: Share more about what’s going on in development.
Follow our public id: Felordcn for more information
Personal blog: https://felord.cn