Mybatis @MapKey annotation returns the specified Map source code analysis and use cases

Article directory

    • Preface
    • Technology accumulation
      • What is MyBatis
      • @MapKey annotation
    • Use case display
    • MapKey annotation source code analysis
    • write at the end

Foreword

A business function being developed recently needs to extract data based on business fields from a batch of data. For this requirement, some students may directly use for or stream loops to process it. However, as a senior bricklayer, adhering to the diligent thinking of never writing by hand to be able to complete the framework, we can use Mybatis to call the database to query the data and directly use the @MapKey annotation to directly encapsulate it into a Map with the business field as the key. Get data directly based on key.

Technology accumulation

What is MyBatis

Needless to say, MyBatis is a persistence layer framework based on Java language. It maps objects to stored procedures or SQL statements through XML descriptors or annotations, and provides operation methods such as ordinary SQL queries, stored procedures, and advanced mapping. , making it very convenient to operate the database.

@MapKey annotation

View @MapKey annotation source code

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MapKey {
  String value();
}

From the above source code, we can see that MapKey annotations are applied to runtime methods, which is the methods of our DAO.
Using the MapKey annotation can directly return a Map with the specified field as the key and the overall data as the value. Through this Map, we can directly obtain the specific data of the specified field.

Use case display

1. Add query sql statement to xml

<resultMap id="businessGroupByUserMap" type="com.ysjr.base.domain.entity.vo.roam.RoamBusinessVo">
    <result column="count" property="count" jdbcType="INTEGER"/>
    <result column="belongUserId" property="belongUserId" jdbcType="BIGINT"/>
</resultMap>
<!--Transfers are grouped according to user inquiry business opportunities-->
<select id="businessGroupByUser" parameterType="map" resultMap="businessGroupByUserMap">
    select count(customer_business_id) as count,belong_user_id as belongUserId from t_customer_business
    where delete_status = 1
      and belong_company_child_id = #{belongCompanyChildId}
      and clue_business_type = 2
      and `status` in ('BS002','BS003','BS004','BS005')
    group by belong_user_id;
</select>

2. DAO adds calling method

/**
 * Circulation business opportunities are grouped according to users
 * @param belongCompanyChildId
 * @author senfel
 * @date 2023/10/27 9:50
 * @return
 */
@MapKey("belongUserId")
Map<Long, RoamBusinessVo> businessGroupByUser(@Param("belongCompanyChildId") Long belongCompanyChildId);

3. Add test cases

/**
 *MapKeyAnnotationTest
 * @author senfel
 * @version 1.0
 * @date 2023/10/27 9:57
 */
@Slf4j
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class MapKeyAnnotationTest {

    @Autowired
    private CustomerBusinessDao customerBusinessDao;

    /**
     *MapKey resultMap
     * @author senfel
     * @date 2023/10/27 9:59
     * @return void
     */
    @Test
    public void resultMap(){
        Map<Long, RoamBusinessVo> longIntegerMap = customerBusinessDao.businessGroupByUser(2l);
        System.err.println(longIntegerMap);

    }
}

4. Run test cases in debug mode

As we know from the above figure, what we call Mybatis to execute the sql statement directly returns the data that has been encapsulated as a Map, and the key of the Map is @MapKey(“belongUserId”) in our annotation, and the data is the queried field.

After obtaining this data structure, we can directly obtain the data of the specified belongUserId in the business code for processing, without any need for looping or other operations.

Of course, some students may ask why not query one item at a time. This must be based on business logic. For example, here we count the amount of business opportunity data based on subsidiaries and group them according to the owner. There are also some queries with large amounts of data in a single query. In situations that affect performance, you need to consider pulling multiple pieces of data from the database at one time.

MapKey annotation source code analysis

The MapKey source code logic is relatively simple. Basically, if we add the @MapKey annotation to the method, the Mybatis framework will help us encapsulate the specified type of Map data based on the specified key.

First, we enter the @MapKey source code to view the classes it references:

Enter MapperMethod to view the specific execution logic. There is a method signature MethodSignature method that obtains the key field in the annotation:

//Method signature, the key field specified in our annotation is obtained in the signature
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
  Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
  if (resolvedReturnType instanceof Class<?>) {
    this.returnType = (Class<?>) resolvedReturnType;
  } else if (resolvedReturnType instanceof ParameterizedType) {
    this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
  } else {
    this.returnType = method.getReturnType();
  }
  this.returnsVoid = void.class.equals(this.returnType);
  this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());
  this.returnsCursor = Cursor.class.equals(this.returnType);
  //Get the specified key field
  this.mapKey = getMapKey(method);
  this.returnsMap = (this.mapKey != null);
  this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
  this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
  this.paramNameResolver = new ParamNameResolver(configuration, method);
}
//Directly obtained the value written by the @MapKey annotation of our dao layer
private String getMapKey(Method method) {
  String mapKey = null;
  if (Map.class.isAssignableFrom(method.getReturnType())) {
    final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
    if (mapKeyAnnotation != null) {
      mapKey = mapKeyAnnotation.value();
    }
  }
  return mapKey;
}

Now that we have obtained the annotation key field, we can continue to search to see where this.mapKey is called.

As shown in the figure above, we see that when Mybatis executes the sql statement, the key field in mapKey is passed in. We continue to view the execution logic:

//Execution returns map
private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) {
  Map<K, V> result;
  Object param = method.convertArgsToSqlCommandParam(args);
  if (method.hasRowBounds()) {
    RowBounds rowBounds = method.extractRowBounds(args);
    result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey(), rowBounds);
  } else {
    result = sqlSession.<K, V>selectMap(command.getName(), param, method.getMapKey());
  }
  return result;
}

Continue below to see how Mybatis encapsulates the results into a Map:

Directly enter the default method to view the execution logic. Here, the default map result processor of DefaultMapResultHandler is used, and the handleResult method is called in a loop for data encapsulation:

//View the method encapsulated as map
@Override
public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
  final List<? extends V> list = selectList(statement, parameter, rowBounds);
  //The source code here uses the default map result processor of DefaultMapResultHandler
  final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<K, V>(mapKey,
      configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
  final DefaultResultContext<V> context = new DefaultResultContext<V>();
  //Loop the objects returned by the database
  for (V o : list) {
    context.nextResultObject(o);
    mapResultHandler.handleResult(context);
  }
  return mapResultHandler.getMappedResults();
}

We directly enter the default map result processor of DefaultMapResultHandler. Looking at the handleResult method, we find that the key we specified is taken out as the Key of the Map, and then the data is used as the Value of the Map:

@SuppressWarnings("unchecked")
public DefaultMapResultHandler(String mapKey, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
  this.objectFactory = objectFactory;
  this.objectWrapperFactory = objectWrapperFactory;
  this.reflectorFactory = reflectorFactory;
  this.mappedResults = objectFactory.create(Map.class);
  this.mapKey = mapKey;
}
//data encapsulation method
@Override
public void handleResult(ResultContext<? extends V> context) {
  final V value = context.getResultObject();
  final MetaObject mo = MetaObject.forObject(value, objectFactory, objectWrapperFactory, reflectorFactory);
  // TODO is that assignment always true?
  final K key = (K) mo.getValue(mapKey);
  mappedResults.put(key, value);
}

Write at the end

The Mybatis framework is a persistence layer framework commonly used in Java backends. The @MapKey annotation can directly return a Map of the specified type. The core principle is to process the data collection in a loop, using the specified field as the key, and the data as the value to directly return a Map for us to use directly to complete specific business functions.

The road is long and long, I will search up and down