mybatis/mybatis-3 · error · CacheException
Not allowed to update a null cache key instance.
Error message
Not allowed to update a null cache key instance.
What it means
CacheKey.NULL_CACHE_KEY is a singleton sentinel returned by CachingExecutor/BaseJdbcStatementHandler when a statement has no cache key (notably for ResultHandler-driven or non-query flows). Calling update(Object) on it is a programming error, so the anonymous subclass overrides update() to always throw CacheException. It exists to make accidental mutation of the shared sentinel loud instead of silently corrupting cache keys for other statements.
Source
Thrown at src/main/java/org/apache/ibatis/cache/CacheKey.java:38
import java.util.List;
import java.util.StringJoiner;
import org.apache.ibatis.reflection.ArrayUtil;
/**
* @author Clinton Begin
*/
public class CacheKey implements Cloneable, Serializable {
private static final long serialVersionUID = 1146682552656046210L;
public static final CacheKey NULL_CACHE_KEY = new CacheKey() {
private static final long serialVersionUID = 1L;
@Override
public void update(Object object) {
throw new CacheException("Not allowed to update a null cache key instance.");
}
@Override
public void updateAll(Object[] objects) {
throw new CacheException("Not allowed to update a null cache key instance.");
}
};
private static final int DEFAULT_MULTIPLIER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier;
private int hashcode;
private long checksum;
private int count;
// 8/21/2017 - Sonarlint flags this as needing to be marked transient. While true if content is not serializable, this
// is not always true and thus should not be marked transient.
private List<Object> updateList;View on GitHub (pinned to 008069adb1)
Solutions
- Check for the sentinel before mutating: if (key != CacheKey.NULL_CACHE_KEY) key.update(...); or clone first
- Re-examine why the key is the sentinel — for statements that bypass the cache, augmenting keys is meaningless; derive your own CacheKey instead of mutating the passed one
- If writing an interceptor, create a new CacheKey from the BoundSql parameters rather than mutating the incoming one
Example fix
// before
key.update(extraParam);
// after
if (key != CacheKey.NULL_CACHE_KEY) {
key.update(extraParam);
} else {
key = new CacheKey(); key.update(boundSql.getSql()); key.update(extraParam);
} Defensive patterns
Strategy: type-guard
Validate before calling
null
Type guard
static boolean isMutableCacheKey(CacheKey k) { return k != null && k != CacheKey.NULL_CACHE_KEY; } Try / catch
if (!isMutableCacheKey(key)) { key = new CacheKey(); } key.update(x); // avoid the throw by construction Prevention
- Never mutate executor-provided cache keys in plugins
- Derive fresh CacheKeys from BoundSql when you need custom keys
When it happens
Trigger: User code (custom Executor, interceptor, or handler) obtains a key from a context where it may be CacheKey.NULL_CACHE_KEY and calls key.update(value) on it — e.g. a plugin intercepting query() that tries to fold extra criteria into the passed-in cache key, or calling getBoundSql/update on a statement executed via a path that supplies the sentinel.
Common situations: Custom mybatis interceptors that augment cache keys for fine-grained caching; code copied from older mybatis versions where the passed key was always a real CacheKey; calling update on a key returned from createCacheKey() for a statement with a ResultHandler that skips caching (FlushCacheRequired / no-cache flows).
Related errors
- Not allowed to update a NullCacheKey instance.
- cache-ref element requires a namespace attribute.
- No cache for namespace '{namespace}' could be found.
- Cache-ref not yet resolved
- Should be specified either value() or name() attribute in th
AI-assisted analysis of mybatis/mybatis-3@008069adb1 (2026-08-14).
Data as JSON: /api/errors/30db4db31b84373a.
Report an issue: GitHub.