Revisit parent-child relationship in jpa map store

Closes #14278
This commit is contained in:
vramik 2022-09-08 11:08:51 +02:00 committed by Hynek Mlnařík
parent c59660ca86
commit e5408884f6
8 changed files with 90 additions and 24 deletions

View file

@ -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 <code>true</code> if the entityVersion was effectively changed, <code>false</code> 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();

View file

@ -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<JpaRootAuthenticationSessionEntity>{
@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;
}

View file

@ -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;
}
}
}
}

View file

@ -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;

View file

@ -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.
* <p/>
* 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.
* <p/>
* 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 <b>same</b> 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<JpaRealmEntity> {
@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;
}

View file

@ -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<JpaUserEntity> {
public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUserConsentEntity, JpaRootEntity, JpaChildEntity<JpaUserEntity> {
@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();

View file

@ -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<JpaUserEntity> {
public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl implements MapUserFederatedIdentityEntity, JpaRootEntity, JpaChildEntity<JpaUserEntity> {
@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();

View file

@ -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<JpaUserSessionEntity> {
@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;
}