How does mybatis plus disable L1 caching

Time:2021-9-13

preface

Friends who have used mybatis plus may know that mybatis plus provides the function of multi tenant plug-in, which allows developers to add tenant statements automatically without writing tenant statements manually. Today’s source of material is a magical problem encountered by business developers when using multi tenant plug-ins

Problem recurrence

The business developer should update the tenant’s password according to the mobile phone number. The code is as follows

 for(Tenant t : tenantList){
            ApplicationChainContext.getCurrentContext().put(ApplicationChainContext.TENANT_ID,t.getId()+"");
            Optional<SaasUser> user = this.findByUserPhone(req.getUserPhone());
            user.ifPresent(u -> {
                count.getAndSet(count.get() + 1);
                LambdaUpdateWrapper<SaasUser> wrapper = new LambdaUpdateWrapper<>();
                String md5Pwd = Md5Utils.hash(req.getNewUserPwd());
                wrapper.eq(SaasUser::getId,user.get().getId());
                wrapper.set(SaasUser::getUserPwd,md5Pwd);
                this.update(wrapper);
            });
        }

It seems no problem from the code, because the multi tenant plug-in is used. When we execute this. Findbyuserphone (req. Getuserphone()); Will automatically bring the tenant’s information. However, a problem was found during execution, as shown in the following figure
How does mybatis plus disable L1 caching
From the figure, we can find that when the user information cannot be queried, no SQL statement appears in subsequent query operations. This shows that the SQL statement is either eaten by the system or the system does not execute SQL

problem analysis

As mentioned earlier, the SQL statement is not printed, which means that the SQL statement is either not executed or eaten by the system. It’s better to go to the official to find the answer to the question rather than guess. Unfortunately, the answer can’t be found on the mybatis plus official website or issue, so we have to track the source code for analysis. Finally, it was found that mybatis plus, as introduced on his official website, only enhanced and did not change. He finally called the query logic and followed the query logic of native mybatis. The core code of the query is as follows

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
                this.clearLocalCache();
            }

            List list;
            try {
                ++this.queryStack;
                list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                if (list != null) {
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }
            } finally {
                --this.queryStack;
            }

            if (this.queryStack == 0) {
                Iterator var8 = this.deferredLoads.iterator();

                while(var8.hasNext()) {
                    BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
                    deferredLoad.load();
                }

                this.deferredLoads.clear();
                if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                    this.clearLocalCache();
                }
            }

            return list;
        }
    }

From the code, we can get an important information as follows

 list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                if (list != null) {
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }

When resulthandler is empty, the value of list is this.localcache.getobject (key), that is, the local cache will be used instead of database query

Problem solving

From the source code, we can know that the native mybatis will use the local cache by default, that is, the so-called first level cache. As an enhanced version of mybatis, the logic of mybatis plus is the same as the native logic of mybatis. How to disable the first level cache of mybatis plus? From the source code analysis, we can know that when the list is empty, the cache will not be used, but the data will be queried. The cache value of list comes from
this.localCache.getObject(key)。 Therefore, the reverse thinking of disabling caching is to either empty the localcache or change the key to make the value obtained by this.localcache.getobject (key) null. Therefore, there are at least two solutions

Scheme 1: empty localcache

How do you empty it?
Through the source code, we can see that there are two places to empty the localcache. One is

if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }

The other is

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }

Therefore, we can clear it by changing configuration. Getlocalcachescope() to state. You can do the following configuration in YML

mybatis-plus:
    configuration:
        local-cache-scope: statement

Scheme 2: change the key of localcache so that the value obtained by this.localcache.getobject (key) is null

Let’s look at the composition of key first

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            CacheKey cacheKey = new CacheKey();
            cacheKey.update(ms.getId());
            cacheKey.update(rowBounds.getOffset());
            cacheKey.update(rowBounds.getLimit());
            cacheKey.update(boundSql.getSql());
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
            Iterator var8 = parameterMappings.iterator();

            while(var8.hasNext()) {
                ParameterMapping parameterMapping = (ParameterMapping)var8.next();
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    String propertyName = parameterMapping.getProperty();
                    Object value;
                    if (boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }

                    cacheKey.update(value);
                }
            }

            if (this.configuration.getEnvironment() != null) {
                cacheKey.update(this.configuration.getEnvironment().getId());
            }

            return cacheKey;
        }
    }

From the source code, we can see that key is composed of statementid + native SQL + value (queried object) + sqlsession.hashcode.

Therefore, to change the key, we can start with the native SQL. Some friends here may say that the tenant IDs are different when they are normal. Since mybatis plus uses the multi tenant plug-in, the SQL statements generated are different, and the keys generated are different. If you trace the source code, you will find that its native SQL does not add tenant information. Therefore, we can add a random number to the SQL statement of the query, as shown below

 public Optional<SaasUser> findByUserPhone(String userPhone) {
        LambdaQueryWrapper<SaasUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SaasUser::getUserPhone,userPhone);
        wrapper.apply("{0} = {0}",UUID.randomUUID().toString());
        SaasUser saasUser = getBaseMapper().selectOne(wrapper);
        return Optional.ofNullable(saasUser);
    }

That is, add more in the original query code statement

wrapper.apply("{0} = {0}",UUID.randomUUID().toString());

The SQL statement is as follows

 Preparing: SELECT id, user_code, user_name, main_accout_flag, user_pwd, user_phone, admin_flag, user_status, last_login_time, login_ip, pwd_update_time, tenant_id, create_date, created_by, created_by_id, last_updated_by, last_updated_by_id, last_update_date, object_version_number, delete_flag FROM saas_user WHERE delete_flag = 0 AND (user_phone = ? AND ? = ?) AND tenant_id = 424210194470490118 
==> Parameters: 111111(String), edcda7fe-ee43-481a-90f7-8b41cb51a3d1(String), edcda7fe-ee43-481a-90f7-8b41cb51a3d1(String)

In this way, the SQL generated each time will be different, resulting in different keys, and then this.localcache.getobject (key) will be empty. In this way, mybatis can query the database every time, so as to disable the first level cache

summary

Scheme 1 is based on global configuration, and scheme 2 is based on local configuration. Personally, it is recommended to compare scheme 2, that is, by adding random values. Because the meaning of configuring the first level cache in mybatis is to provide performance. However, the scheme should be considered from the perspective of business. In order to ensure the correct operation of functions, it is sometimes harmless to sacrifice some performance