{"record":{"id":"9e654eeb410c7f5e","repo":"redisson/redisson","slug":"unable-to-update-read-only-object-9e654e","errorCode":null,"errorMessage":"Unable to update read-only object","messagePattern":"Unable to update read-only object","errorType":"exception","errorClass":"UnsupportedOperationException","httpStatus":null,"severity":"error","filePath":"redisson-hibernate/redisson-hibernate-52/src/main/java/org/redisson/hibernate/strategy/ReadOnlyNaturalIdRegionAccessStrategy.java","lineNumber":83,"sourceCode":"    @Override\n    public NaturalIdRegion getRegion() {\n        return (NaturalIdRegion) region;\n    }\n\n    @Override\n    public boolean insert(SharedSessionContractImplementor session, Object key, Object value) throws CacheException {\n        return false;\n    }\n\n    @Override\n    public boolean afterInsert(SharedSessionContractImplementor session, Object key, Object value) throws CacheException {\n        region.put(session, key, value);\n        return true;\n    }\n\n    @Override\n    public boolean update(SharedSessionContractImplementor session, Object key, Object value) throws CacheException {\n        throw new UnsupportedOperationException(\"Unable to update read-only object\");\n    }\n\n    @Override\n    public boolean afterUpdate(SharedSessionContractImplementor session, Object key, Object value, SoftLock lock) throws CacheException {\n        throw new UnsupportedOperationException(\"Unable to update read-only object\");\n    }\n\n    @Override\n    public Object generateCacheKey(Object[] naturalIdValues, EntityPersister persister, SharedSessionContractImplementor session) {\n        return ((RedissonNaturalIdRegion)region).getCacheKeysFactory().createNaturalIdKey(naturalIdValues, persister, session);\n    }\n\n    @Override\n    public Object[] getNaturalIdValues(Object cacheKey) {\n        return ((RedissonNaturalIdRegion)region).getCacheKeysFactory().getNaturalIdValues(cacheKey);\n    }\n\n}","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/redisson/redisson/blob/91188987c2a9023e7fedabff758681ad9b107f25/redisson-hibernate/redisson-hibernate-52/src/main/java/org/redisson/hibernate/strategy/ReadOnlyNaturalIdRegionAccessStrategy.java#L65-L101","documentation":"Thrown by ReadOnlyNaturalIdRegionAccessStrategy.update() (Hibernate 5.2 module) when Hibernate tries to propagate an update of an entity's natural id into a natural-id cache region configured with a read-only strategy. Natural-id read-only regions support insert-time population (afterInsert) but no update path, so update() throws UnsupportedOperationException. The trigger is always that the natural id of a READ_ONLY-cached entity is being changed.","triggerScenarios":"An entity with @NaturalId fields and @NaturalIdCache (region resolved with read-only usage) has its natural-id property modified and flushed — Hibernate invokes update(key, value) on the natural-id access strategy.","commonSituations":"Natural keys assumed immutable (email, SSN, SKU) that the application later allows editing; default read-only strategy applied globally while one entity legitimately renames its natural key.","solutions":["Treat natural ids as immutable by design: remove setters for the @NaturalId field and re-create entities when the key changes.","If the natural id must be mutable, switch the owning entity's cache to a mutable strategy (@Cache(usage = NONSTRICT_READ_WRITE)) so the natural-id region gets a matching strategy.","Add an integration test that attempts to change each @NaturalId field so regressions fail fast in CI, not in production."],"exampleFix":"// before\n@Entity\n@NaturalIdCache\npublic class Employee { @NaturalId String employeeNo; void setEmployeeNo(String v){...} }\n\n// after\n@Entity\n@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)\n@NaturalIdCache\npublic class Employee { @NaturalId String employeeNo; /* no setter: immutable key */ }","handlingStrategy":"validation","validationCode":"// design-time rule: @NaturalId fields must have no setters\nboolean naturalIdIsImmutable(Class<?> entity, java.lang.reflect.Field f) {\n    return f.isAnnotationPresent(org.hibernate.annotations.NaturalId.class)\n        && java.util.Arrays.stream(entity.getMethods())\n            .noneMatch(m -> m.getName().equalsIgnoreCase(\"set\" + f.getName()));\n}","typeGuard":null,"tryCatchPattern":"try {\n    session.flush(); // natural id of READ_ONLY-cached entity was changed\n} catch (UnsupportedOperationException e) {\n    if (\"Unable to update read-only object\".equals(e.getMessage())) {\n        // natural key changed: either treat key as immutable or use a mutable strategy\n    }\n    throw e;\n}","preventionTips":["Model natural keys as immutable values; 'change' means delete + insert.","Add @Column(updatable = false) on @NaturalId fields so Hibernate itself rejects updates.","Integration-test every @NaturalId field for attempted mutation."],"tags":["redisson","hibernate","natural-id","read-only","runtime"],"backgroundTag":null,"analyzedSha":"91188987c2a9023e7fedabff758681ad9b107f25","analyzedAt":"2026-08-14T11:39:42.619Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}