diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaRootEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaRootEntity.java
index add757ddf9..643ee74c9a 100644
--- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaRootEntity.java
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaRootEntity.java
@@ -46,13 +46,17 @@ public interface JpaRootEntity extends AbstractEntity, Serializable {
* calls this method whenever the root entity or one of its children changes.
*
* Future versions of this method might restrict downgrading to downgrade only from the next version.
+ *
+ * @return true
if the entityVersion was effectively changed, false
otherwise.
*/
- default void updateEntityVersion() {
+ default boolean updateEntityVersion() {
Integer ev = getEntityVersion();
Integer currentEv = getCurrentSchemaVersion();
if (ev != null && !Objects.equals(ev, currentEv)) {
setEntityVersion(currentEv);
+ return true;
}
+ return false;
}
Integer getCurrentSchemaVersion();
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/entity/JpaAuthenticationSessionEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/entity/JpaAuthenticationSessionEntity.java
index c1dd469249..f14492caa9 100644
--- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/entity/JpaAuthenticationSessionEntity.java
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/entity/JpaAuthenticationSessionEntity.java
@@ -37,6 +37,7 @@ import org.keycloak.models.map.authSession.MapAuthenticationSessionEntity;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.UpdatableEntity;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTH_SESSION;
+import org.keycloak.models.map.storage.jpa.JpaChildEntity;
import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import org.keycloak.sessions.CommonClientSessionModel;
@@ -47,7 +48,7 @@ import org.keycloak.sessions.CommonClientSessionModel;
@Entity
@Table(name = "kc_auth_session")
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
-public class JpaAuthenticationSessionEntity extends UpdatableEntity.Impl implements MapAuthenticationSessionEntity, JpaRootVersionedEntity {
+public class JpaAuthenticationSessionEntity extends UpdatableEntity.Impl implements MapAuthenticationSessionEntity, JpaRootVersionedEntity, JpaChildEntity{
@Id
@Column
@@ -86,6 +87,11 @@ public class JpaAuthenticationSessionEntity extends UpdatableEntity.Impl impleme
return metadata != null;
}
+ @Override
+ public JpaRootAuthenticationSessionEntity getParent() {
+ return root;
+ }
+
public void setParent(JpaRootAuthenticationSessionEntity root) {
this.root = root;
}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaEntityVersionListener.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaEntityVersionListener.java
index 2749ea01d7..e4aff1bc3a 100644
--- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaEntityVersionListener.java
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaEntityVersionListener.java
@@ -30,22 +30,30 @@ import org.keycloak.models.map.storage.jpa.JpaRootEntity;
/**
* Listen on changes on child- and root entities and updates the current entity version of the root.
*
- * This support a multiple level parent-child relationship, where the upmost parent needs the entity version to be updated.
+ * This support a multiple level parent-child relationship, where all parents needs the entity version to be updated
+ * in case it was effectively changed. The traversing is stopped at that point when it is detected that parent entity
+ * version is the same one.
+ *
+ * It is based on an assumption that it may happen that one parent entity could be extracted into "parent A -> parent B -> child"
+ * format. Then the change (insertion, deletion or update) of the child should bump the entity version of both parent B and parent A.
*/
public class JpaEntityVersionListener implements PreInsertEventListener, PreDeleteEventListener, PreUpdateEventListener {
public static final JpaEntityVersionListener INSTANCE = new JpaEntityVersionListener();
/**
- * Traverse from current entity up to the upmost parent, then update the entity version if it is a root entity.
+ * Traverse from current entity through its parent tree and update the entity version of it.
+ * Stop if non-changed parent is found.
*/
public void updateEntityVersion(Object entity) throws HibernateException {
Object root = entity;
while(root instanceof JpaChildEntity) {
root = ((JpaChildEntity>) entity).getParent();
- }
- if (root instanceof JpaRootEntity) {
- ((JpaRootEntity) root).updateEntityVersion();
+ if (root instanceof JpaRootEntity) {
+ if (!((JpaRootEntity) root).updateEntityVersion()) {
+ return;
+ }
+ }
}
}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaOptimisticLockingListener.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaOptimisticLockingListener.java
index c582634051..fbfbbc0942 100644
--- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaOptimisticLockingListener.java
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaOptimisticLockingListener.java
@@ -30,26 +30,27 @@ import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity;
import javax.persistence.LockModeType;
import java.util.Objects;
+import org.keycloak.models.map.storage.jpa.JpaRootEntity;
/**
- * Listen on changes on child entities and forces an optimistic locking increment on the topmost parent aka root.
+ * Listen on changes on child entities and forces an optimistic locking increment on the closest parent aka root.
+ * The assumption is that any parent of a child entity is root entity. Optimistic locking is enforced on child entity
+ * which is not the child entity at the same time. This prevents {@link javax.persistence.OptimisticLockException}s
+ * when different children in the same parent are being manipulated at the same time by different threads.
*
- * This support a multiple level parent-child relationship, where only the upmost parent is locked.
+ * This support a multiple level parent-child relationship, where only the closest parent is locked.
*/
public class JpaOptimisticLockingListener implements PreInsertEventListener, PreDeleteEventListener, PreUpdateEventListener {
public static final JpaOptimisticLockingListener INSTANCE = new JpaOptimisticLockingListener();
/**
- * Check if the entity is a child with a parent and force optimistic locking increment on the upmost parent aka root.
+ * Check if the entity is a child with a parent and force optimistic locking increment on the parent aka root.
*/
public void lockRootEntity(Session session, Object entity) throws HibernateException {
- if(entity instanceof JpaChildEntity) {
- Object root = entity;
- while (root instanceof JpaChildEntity) {
- root = ((JpaChildEntity>) entity).getParent();
- Objects.requireNonNull(root, "children must always return their parent, never null");
- }
+ if (entity instanceof JpaChildEntity && ! (entity instanceof JpaRootEntity)) {
+ Object root = ((JpaChildEntity>) entity).getParent();
+ Objects.requireNonNull(root, "children must always return their parent, never null");
// do not lock if root doesn't implement implicit optimistic locking mechanism
if (! (root instanceof JpaRootVersionedEntity)) return;
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/entity/JpaComponentEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/entity/JpaComponentEntity.java
index ca4d991a00..1222dc9a03 100644
--- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/entity/JpaComponentEntity.java
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/entity/JpaComponentEntity.java
@@ -39,6 +39,7 @@ import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.map.common.UuidValidator;
import org.keycloak.models.map.realm.entity.MapComponentEntity;
import org.keycloak.models.map.storage.jpa.Constants;
+import org.keycloak.models.map.storage.jpa.JpaChildEntity;
import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
@@ -48,10 +49,7 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
* to indicate that they are automatically generated from json fields. As such, these fields are non-insertable and non-updatable.
*
* Components are independent (i.e. a component doesn't depend on another component) and can be manipulated directly via
- * the component endpoints. Because of that, this entity implements {@link JpaRootVersionedEntity} instead of
- * {@link org.keycloak.models.map.storage.jpa.JpaChildEntity}. This prevents {@link javax.persistence.OptimisticLockException}s
- * when different components in the same realm are being manipulated at the same time - for example, when multiple components
- * are being added to the realm by different threads.
+ * the component endpoints.
*
* By implementing {@link JpaRootVersionedEntity}, this entity will enforce optimistic locking, which can lead to
* {@link javax.persistence.OptimisticLockException} if more than one thread attempts to modify the same component
@@ -62,7 +60,7 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
@Entity
@Table(name = "kc_component")
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
-public class JpaComponentEntity extends UpdatableEntity.Impl implements MapComponentEntity, JpaRootVersionedEntity {
+public class JpaComponentEntity extends UpdatableEntity.Impl implements MapComponentEntity, JpaRootVersionedEntity, JpaChildEntity {
@Id
@Column
@@ -100,6 +98,11 @@ public class JpaComponentEntity extends UpdatableEntity.Impl implements MapCompo
this.metadata = new JpaComponentMetadata(cloner);
}
+ @Override
+ public JpaRealmEntity getParent() {
+ return root;
+ }
+
public void setParent(JpaRealmEntity root) {
this.root = root;
}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserConsentEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserConsentEntity.java
index 9c2aa1def6..8e31c707fb 100644
--- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserConsentEntity.java
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserConsentEntity.java
@@ -36,7 +36,9 @@ import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.UpdatableEntity;
+import org.keycloak.models.map.storage.jpa.Constants;
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
+import org.keycloak.models.map.storage.jpa.JpaRootEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import org.keycloak.models.map.user.MapUserConsentEntity;
@@ -52,7 +54,7 @@ import org.keycloak.models.map.user.MapUserConsentEntity;
@UniqueConstraint(columnNames = {"clientId"})
})
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
-public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUserConsentEntity, JpaChildEntity {
+public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUserConsentEntity, JpaRootEntity, JpaChildEntity {
@Id
@Column
@@ -87,11 +89,13 @@ public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUse
return metadata != null;
}
+ @Override
public Integer getEntityVersion() {
if (isMetadataInitialized()) return this.metadata.getEntityVersion();
return entityVersion;
}
+ @Override
public void setEntityVersion(Integer version) {
this.metadata.setEntityVersion(version);
}
@@ -105,6 +109,21 @@ public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUse
this.root = root;
}
+ @Override
+ public String getId() {
+ return id == null ? null : id.toString();
+ }
+
+ @Override
+ public void setId(String id) {
+ this.id = id == null ? null : UUID.fromString(id);
+ }
+
+ @Override
+ public Integer getCurrentSchemaVersion() {
+ return Constants.CURRENT_SCHEMA_VERSION_USER_CONSENT;
+ }
+
@Override
public String getClientId() {
if (isMetadataInitialized()) return this.metadata.getClientId();
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserFederatedIdentityEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserFederatedIdentityEntity.java
index e43346fc25..d9e3c44ea5 100644
--- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserFederatedIdentityEntity.java
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserFederatedIdentityEntity.java
@@ -34,7 +34,9 @@ import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.UpdatableEntity;
+import org.keycloak.models.map.storage.jpa.Constants;
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
+import org.keycloak.models.map.storage.jpa.JpaRootEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import org.keycloak.models.map.user.MapUserFederatedIdentityEntity;
@@ -47,7 +49,7 @@ import org.keycloak.models.map.user.MapUserFederatedIdentityEntity;
@Entity
@Table(name = "kc_user_federated_identity")
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
-public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl implements MapUserFederatedIdentityEntity, JpaChildEntity {
+public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl implements MapUserFederatedIdentityEntity, JpaRootEntity, JpaChildEntity {
@Id
@Column
@@ -86,15 +88,22 @@ public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl impleme
return metadata != null;
}
+ @Override
public Integer getEntityVersion() {
if (isMetadataInitialized()) return this.metadata.getEntityVersion();
return entityVersion;
}
+ @Override
public void setEntityVersion(Integer version) {
this.metadata.setEntityVersion(version);
}
+ @Override
+ public Integer getCurrentSchemaVersion() {
+ return Constants.CURRENT_SCHEMA_VERSION_USER_FEDERATED_IDENTITY;
+ }
+
@Override
public JpaUserEntity getParent() {
return this.root;
@@ -104,6 +113,16 @@ public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl impleme
this.root = root;
}
+ @Override
+ public String getId() {
+ return id == null ? null : id.toString();
+ }
+
+ @Override
+ public void setId(String id) {
+ this.id = id == null ? null : UUID.fromString(id);
+ }
+
@Override
public String getToken() {
return this.metadata.getToken();
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/entity/JpaClientSessionEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/entity/JpaClientSessionEntity.java
index bd7bb54683..d2f6070201 100644
--- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/entity/JpaClientSessionEntity.java
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/entity/JpaClientSessionEntity.java
@@ -39,6 +39,7 @@ import org.hibernate.annotations.TypeDefs;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.UuidValidator;
import org.keycloak.models.map.storage.jpa.Constants;
+import org.keycloak.models.map.storage.jpa.JpaChildEntity;
import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity.AbstractAuthenticatedClientSessionEntity;
@@ -49,7 +50,7 @@ import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity.A
@Entity
@Table(name = "kc_client_session")
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
-public class JpaClientSessionEntity extends AbstractAuthenticatedClientSessionEntity implements JpaRootVersionedEntity {
+public class JpaClientSessionEntity extends AbstractAuthenticatedClientSessionEntity implements JpaRootVersionedEntity, JpaChildEntity {
@Id
@Column
@@ -94,6 +95,11 @@ public class JpaClientSessionEntity extends AbstractAuthenticatedClientSessionEn
return metadata != null;
}
+ @Override
+ public JpaUserSessionEntity getParent() {
+ return root;
+ }
+
public void setParent(JpaUserSessionEntity root) {
this.root = root;
}