Ensure that parent's version ID is incremented when an attribute changes.
This is necessary to allow the optimistic locking functionality to work as expected when changing only attributes on an entity. Closes #9874
This commit is contained in:
parent
13e02d5f09
commit
9d46b45a9c
7 changed files with 146 additions and 8 deletions
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2022. Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.models.map.storage.jpa;
|
||||
|
||||
/**
|
||||
* Interface for all child entities for JPA map storage.
|
||||
*/
|
||||
public interface JpaChildEntity<R> {
|
||||
|
||||
/**
|
||||
* Parent entity that should get its optimistic locking version updated upon changes in the child
|
||||
*/
|
||||
R getParent();
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright 2022. Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.map.storage.jpa;
|
||||
|
||||
import org.hibernate.HibernateException;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.event.spi.PreDeleteEvent;
|
||||
import org.hibernate.event.spi.PreDeleteEventListener;
|
||||
import org.hibernate.event.spi.PreInsertEvent;
|
||||
import org.hibernate.event.spi.PreInsertEventListener;
|
||||
import org.hibernate.event.spi.PreUpdateEvent;
|
||||
import org.hibernate.event.spi.PreUpdateEventListener;
|
||||
|
||||
import javax.persistence.LockModeType;
|
||||
|
||||
/**
|
||||
* Listen on changes on child entities and forces an optimistic locking increment on the topmost parent.
|
||||
*
|
||||
* This support a multiple level parent-child relationship, where only the upmost parent is locked.
|
||||
*/
|
||||
public class JpaChildEntityListener implements PreInsertEventListener, PreDeleteEventListener, PreUpdateEventListener {
|
||||
|
||||
public static final JpaChildEntityListener INSTANCE = new JpaChildEntityListener();
|
||||
|
||||
/**
|
||||
* Check if the entity is a child with a parent and force optimistic locking increment on the upmost parent.
|
||||
*/
|
||||
public void checkRoot(Session session, Object entity) throws HibernateException {
|
||||
if(entity instanceof JpaChildEntity) {
|
||||
Object root = entity;
|
||||
while (root instanceof JpaChildEntity) {
|
||||
root = ((JpaChildEntity<?>) entity).getParent();
|
||||
}
|
||||
session.lock(root, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreInsert(PreInsertEvent event) {
|
||||
checkRoot(event.getSession(), event.getEntity());
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreDelete(PreDeleteEvent event) {
|
||||
checkRoot(event.getSession(), event.getEntity());
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreUpdate(PreUpdateEvent event) {
|
||||
checkRoot(event.getSession(), event.getEntity());
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import java.sql.Connection;
|
|||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
@ -30,7 +31,14 @@ import javax.persistence.EntityManager;
|
|||
import javax.persistence.EntityManagerFactory;
|
||||
import javax.persistence.Persistence;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.hibernate.boot.Metadata;
|
||||
import org.hibernate.cfg.AvailableSettings;
|
||||
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||
import org.hibernate.event.service.spi.EventListenerRegistry;
|
||||
import org.hibernate.event.spi.EventType;
|
||||
import org.hibernate.jpa.boot.spi.IntegratorProvider;
|
||||
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
|
@ -166,6 +174,31 @@ public class JpaMapStorageProviderFactory implements
|
|||
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
|
||||
properties.put("hibernate.dialect", config.get("driverDialect"));
|
||||
|
||||
properties.put(
|
||||
"hibernate.integrator_provider",
|
||||
(IntegratorProvider) () -> Collections.singletonList(
|
||||
new org.hibernate.integrator.spi.Integrator() {
|
||||
|
||||
@Override
|
||||
public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactoryImplementor,
|
||||
SessionFactoryServiceRegistry sessionFactoryServiceRegistry) {
|
||||
final EventListenerRegistry eventListenerRegistry =
|
||||
sessionFactoryServiceRegistry.getService( EventListenerRegistry.class );
|
||||
|
||||
eventListenerRegistry.appendListeners(EventType.PRE_INSERT, JpaChildEntityListener.INSTANCE);
|
||||
eventListenerRegistry.appendListeners(EventType.PRE_UPDATE, JpaChildEntityListener.INSTANCE);
|
||||
eventListenerRegistry.appendListeners(EventType.PRE_DELETE, JpaChildEntityListener.INSTANCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disintegrate(SessionFactoryImplementor sessionFactoryImplementor,
|
||||
SessionFactoryServiceRegistry sessionFactoryServiceRegistry) {
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Integer isolation = config.getInt("isolation");
|
||||
if (isolation != null) {
|
||||
if (isolation < Connection.TRANSACTION_REPEATABLE_READ) {
|
||||
|
|
|
@ -28,10 +28,11 @@ import javax.persistence.JoinColumn;
|
|||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.Table;
|
||||
import org.hibernate.annotations.Nationalized;
|
||||
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
|
||||
|
||||
@Entity
|
||||
@Table(name = "client_attribute")
|
||||
public class JpaClientAttributeEntity implements Serializable {
|
||||
public class JpaClientAttributeEntity implements JpaChildEntity<JpaClientEntity>, Serializable {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
|
@ -100,4 +101,9 @@ public class JpaClientAttributeEntity implements Serializable {
|
|||
Objects.equals(getName(), that.getName()) &&
|
||||
Objects.equals(getValue(), that.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JpaClientEntity getParent() {
|
||||
return getClient();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -587,7 +587,6 @@ public class JpaClientEntity extends AbstractClientEntity implements JpaRootEnti
|
|||
JpaClientAttributeEntity attr = iterator.next();
|
||||
if (Objects.equals(attr.getName(), name)) {
|
||||
iterator.remove();
|
||||
attr.setClient(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -625,9 +624,7 @@ public class JpaClientEntity extends AbstractClientEntity implements JpaRootEnti
|
|||
public void setAttributes(Map<String, List<String>> attributes) {
|
||||
checkEntityVersionForUpdate();
|
||||
for (Iterator<JpaClientAttributeEntity> iterator = this.attributes.iterator(); iterator.hasNext();) {
|
||||
JpaClientAttributeEntity attr = iterator.next();
|
||||
iterator.remove();
|
||||
attr.setClient(null);
|
||||
}
|
||||
if (attributes != null) {
|
||||
for (Map.Entry<String, List<String>> attrEntry : attributes.entrySet()) {
|
||||
|
|
|
@ -28,10 +28,12 @@ import javax.persistence.JoinColumn;
|
|||
import javax.persistence.ManyToOne;
|
||||
import javax.persistence.Table;
|
||||
import org.hibernate.annotations.Nationalized;
|
||||
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
|
||||
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
|
||||
|
||||
@Entity
|
||||
@Table(name = "role_attribute")
|
||||
public class JpaRoleAttributeEntity implements Serializable {
|
||||
public class JpaRoleAttributeEntity implements JpaChildEntity<JpaRoleEntity>, Serializable {
|
||||
|
||||
@Id
|
||||
@Column
|
||||
|
@ -100,4 +102,9 @@ public class JpaRoleAttributeEntity implements Serializable {
|
|||
Objects.equals(getName(), that.getName()) &&
|
||||
Objects.equals(getValue(), that.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JpaRoleEntity getParent() {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -257,9 +257,7 @@ public class JpaRoleEntity extends AbstractRoleEntity implements JpaRootEntity {
|
|||
public void setAttributes(Map<String, List<String>> attributes) {
|
||||
checkEntityVersionForUpdate();
|
||||
for (Iterator<JpaRoleAttributeEntity> iterator = this.attributes.iterator(); iterator.hasNext();) {
|
||||
JpaRoleAttributeEntity attr = iterator.next();
|
||||
iterator.remove();
|
||||
attr.setRole(null);
|
||||
}
|
||||
if (attributes != null) {
|
||||
for (Map.Entry<String, List<String>> entry : attributes.entrySet()) {
|
||||
|
@ -285,7 +283,6 @@ public class JpaRoleEntity extends AbstractRoleEntity implements JpaRootEntity {
|
|||
JpaRoleAttributeEntity attr = iterator.next();
|
||||
if (Objects.equals(attr.getName(), name)) {
|
||||
iterator.remove();
|
||||
attr.setRole(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue