concurrent transaction fix

This commit is contained in:
Bill Burke 2016-03-02 16:55:55 -05:00
parent c584a1a161
commit a13bac4c9d
10 changed files with 124 additions and 39 deletions

View file

@ -47,7 +47,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ClientAdapter implements ClientModel {
public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
protected KeycloakSession session;
protected RealmModel realm;

View file

@ -24,6 +24,7 @@ import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.jpa.entities.ClientEntity;
import org.keycloak.models.jpa.entities.ClientTemplateEntity;
import org.keycloak.models.jpa.entities.ProtocolMapperEntity;
import org.keycloak.models.jpa.entities.RoleEntity;
@ -42,7 +43,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ClientTemplateAdapter implements ClientTemplateModel {
public class ClientTemplateAdapter implements ClientTemplateModel , JpaModel<ClientTemplateEntity> {
protected KeycloakSession session;
protected RealmModel realm;

View file

@ -41,7 +41,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class GroupAdapter implements GroupModel {
public class GroupAdapter implements GroupModel , JpaModel<GroupEntity> {
protected GroupEntity group;
protected EntityManager em;
@ -53,7 +53,7 @@ public class GroupAdapter implements GroupModel {
this.realm = realm;
}
public GroupEntity getGroup() {
public GroupEntity getEntity() {
return group;
}
@ -88,7 +88,7 @@ public class GroupAdapter implements GroupModel {
public static GroupEntity toEntity(GroupModel model, EntityManager em) {
if (model instanceof GroupAdapter) {
return ((GroupAdapter)model).getGroup();
return ((GroupAdapter)model).getEntity();
}
return em.getReference(GroupEntity.class, model.getId());
}
@ -233,7 +233,7 @@ public class GroupAdapter implements GroupModel {
protected TypedQuery<GroupRoleMappingEntity> getGroupRoleMappingEntityTypedQuery(RoleModel role) {
TypedQuery<GroupRoleMappingEntity> query = em.createNamedQuery("groupHasRole", GroupRoleMappingEntity.class);
query.setParameter("group", getGroup());
query.setParameter("group", getEntity());
query.setParameter("roleId", role.getId());
return query;
}
@ -242,7 +242,7 @@ public class GroupAdapter implements GroupModel {
public void grantRole(RoleModel role) {
if (hasRole(role)) return;
GroupRoleMappingEntity entity = new GroupRoleMappingEntity();
entity.setGroup(getGroup());
entity.setGroup(getEntity());
entity.setRoleId(role.getId());
em.persist(entity);
em.flush();
@ -269,7 +269,7 @@ public class GroupAdapter implements GroupModel {
// we query ids only as the role might be cached and following the @ManyToOne will result in a load
// even if we're getting just the id.
TypedQuery<String> query = em.createNamedQuery("groupRoleMappingIds", String.class);
query.setParameter("group", getGroup());
query.setParameter("group", getEntity());
List<String> ids = query.getResultList();
Set<RoleModel> roles = new HashSet<RoleModel>();
for (String roleId : ids) {

View file

@ -0,0 +1,9 @@
package org.keycloak.models.jpa;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface JpaModel<T> {
T getEntity();
}

View file

@ -39,9 +39,11 @@ import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@ -53,6 +55,14 @@ public class JpaRealmProvider implements RealmProvider {
private final KeycloakSession session;
protected EntityManager em;
// we have a local map of adapter classes for two reasons
// 1. So we don't have to allocate one again
// 2. So that we can do em.refresh() on the entity to make sure the state
// is up to date. If em.find is called a second time without a session, the same
// entity instance is returned. With the caching layer above it, it will cache stale
// entries.
protected Map<String, Object> adapters = new HashMap<>();
public JpaRealmProvider(KeycloakSession session, EntityManager em) {
this.session = session;
@ -76,21 +86,28 @@ public class JpaRealmProvider implements RealmProvider {
realm.setId(id);
em.persist(realm);
em.flush();
final RealmModel model = new RealmAdapter(session, em, realm);
final RealmModel adapter = new RealmAdapter(session, em, realm);
session.getKeycloakSessionFactory().publish(new RealmModel.RealmCreationEvent() {
@Override
public RealmModel getCreatedRealm() {
return model;
return adapter;
}
});
return model;
adapters.put(id, adapter);
return adapter;
}
@Override
public RealmModel getRealm(String id) {
RealmAdapter adapter = (RealmAdapter)findJpaModel(id);
if (adapter != null) {
return adapter;
}
RealmEntity realm = em.find(RealmEntity.class, id);
if (realm == null) return null;
return new RealmAdapter(session, em, realm);
adapter = new RealmAdapter(session, em, realm);
adapters.put(id, adapter);
return adapter;
}
@Override
@ -124,8 +141,10 @@ public class JpaRealmProvider implements RealmProvider {
if (realm == null) {
return false;
}
em.refresh(realm);
RealmAdapter adapter = new RealmAdapter(session, em, realm);
session.users().preRemove(adapter);
adapters.remove(id);
int num = em.createNamedQuery("deleteGroupRoleMappingsByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteGroupAttributesByRealm")
@ -174,7 +193,9 @@ public class JpaRealmProvider implements RealmProvider {
entity.setRealmId(realm.getId());
em.persist(entity);
em.flush();
return new RoleAdapter(session, realm, em, entity);
RoleAdapter adapter = new RoleAdapter(session, realm, em, entity);
adapters.put(id, adapter);
return adapter;
}
@ -203,7 +224,9 @@ public class JpaRealmProvider implements RealmProvider {
roleEntity.setRealmId(realm.getId());
em.persist(roleEntity);
em.flush();
return new RoleAdapter(session, realm, em, roleEntity);
RoleAdapter adapter = new RoleAdapter(session, realm, em, roleEntity);
adapters.put(id, adapter);
return adapter;
}
@Override
@ -247,11 +270,12 @@ public class JpaRealmProvider implements RealmProvider {
@Override
public boolean removeRole(RealmModel realm, RoleModel role) {
session.users().preRemove(realm, role);
RoleEntity roleEntity = em.getReference(RoleEntity.class, role.getId());
RoleContainerModel container = role.getContainer();
if (container.getDefaultRoles().contains(role.getName())) {
container.removeDefaultRoles(role.getName());
}
adapters.remove(role.getId());
RoleEntity roleEntity = em.getReference(RoleEntity.class, role.getId());
String compositeRoleTable = JpaUtils.getTableNameForNativeQuery("COMPOSITE_ROLE", em);
em.createNativeQuery("delete from " + compositeRoleTable + " where CHILD_ROLE = :role").setParameter("role", roleEntity).executeUpdate();
em.createNamedQuery("deleteScopeMappingByRole").setParameter("role", roleEntity).executeUpdate();
@ -260,25 +284,35 @@ public class JpaRealmProvider implements RealmProvider {
em.remove(roleEntity);
em.flush();
return true;
}
@Override
public RoleModel getRoleById(String id, RealmModel realm) {
RoleAdapter adapter = (RoleAdapter)findJpaModel(id);
if (adapter != null) {
if (!realm.getId().equals(adapter.getEntity().getRealmId())) return null;
return adapter;
}
RoleEntity entity = em.find(RoleEntity.class, id);
if (entity == null) return null;
if (!realm.getId().equals(entity.getRealmId())) return null;
return new RoleAdapter(session, realm, em, entity);
adapter = new RoleAdapter(session, realm, em, entity);
adapters.put(id, adapter);
return adapter;
}
@Override
public GroupModel getGroupById(String id, RealmModel realm) {
GroupAdapter adapter = (GroupAdapter)findJpaModel(id);
if (adapter != null) return adapter;
GroupEntity groupEntity = em.find(GroupEntity.class, id);
if (groupEntity == null) return null;
if (!groupEntity.getRealm().getId().equals(realm.getId())) return null;
return new GroupAdapter(realm, em, groupEntity);
adapter = new GroupAdapter(realm, em, groupEntity);
adapters.put(id, adapter);
return adapter;
}
@Override
@ -326,6 +360,7 @@ public class JpaRealmProvider implements RealmProvider {
}
session.users().preRemove(realm, group);
adapters.remove(group.getId());
realm.removeDefaultGroup(group);
for (GroupModel subGroup : group.getSubGroups()) {
@ -361,7 +396,9 @@ public class JpaRealmProvider implements RealmProvider {
groupEntity.setRealm(realmEntity);
em.persist(groupEntity);
return new GroupAdapter(realm, em, groupEntity);
GroupAdapter adapter = new GroupAdapter(realm, em, groupEntity);
adapters.put(id, adapter);
return adapter;
}
@Override
@ -391,6 +428,8 @@ public class JpaRealmProvider implements RealmProvider {
em.persist(entity);
em.flush();
final ClientModel resource = new ClientAdapter(realm, em, session, entity);
adapters.put(id, resource);
em.flush();
session.getKeycloakSessionFactory().publish(new RealmModel.ClientCreationEvent() {
@Override
@ -416,14 +455,39 @@ public class JpaRealmProvider implements RealmProvider {
}
protected JpaModel findJpaModel(String id) {
// we have a local map of adapter classes for two reasons
// 1. So we don't have to allocate one again
// 2. So that we can do em.refresh() on the entity to make sure the state
// is up to date. If em.find is called a second time without a session, the same
// entity instance is returned as its already in the first level cache. With the caching layer above it, it will cache stale
// entries.
JpaModel client = (JpaModel)adapters.get(id);
if (client != null) {
if (em.contains(client.getEntity())) {
em.flush(); // have to flush as refresh blows away updates
em.refresh(client.getEntity());
return client;
}
}
return null;
}
@Override
public ClientModel getClientById(String id, RealmModel realm) {
ClientAdapter client = (ClientAdapter)findJpaModel(id);
if (client != null) {
if (!realm.getId().equals(client.getRealm().getId())) return null;
return client;
}
ClientEntity app = em.find(ClientEntity.class, id);
// Check if application belongs to this realm
if (app == null || !realm.getId().equals(app.getRealm().getId())) return null;
return new ClientAdapter(realm, em, session, app);
client = new ClientAdapter(realm, em, session, app);
adapters.put(id, client);
return client;
}
@Override
@ -459,15 +523,23 @@ public class JpaRealmProvider implements RealmProvider {
logger.errorv("Unable to delete client entity: {0} from realm {1}", client.getClientId(), realm.getName());
throw e;
}
adapters.remove(id);
return true;
}
@Override
public ClientTemplateModel getClientTemplateById(String id, RealmModel realm) {
ClientTemplateAdapter adapter = (ClientTemplateAdapter)findJpaModel(id);
if (adapter != null) {
if (!realm.getId().equals(adapter.getRealm().getId())) return null;
return adapter;
}
ClientTemplateEntity app = em.find(ClientTemplateEntity.class, id);
// Check if application belongs to this realm
if (app == null || !realm.getId().equals(app.getRealm().getId())) return null;
return new ClientTemplateAdapter(realm, em, session, app);
adapter = new ClientTemplateAdapter(realm, em, session, app);
adapters.put(id, adapter);
return adapter;
}
}

View file

@ -65,7 +65,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RealmAdapter implements RealmModel {
public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
protected static final Logger logger = Logger.getLogger(RealmAdapter.class);
protected RealmEntity realm;
protected EntityManager em;

View file

@ -21,6 +21,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.jpa.entities.RealmEntity;
import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
@ -33,7 +34,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RoleAdapter implements RoleModel {
public class RoleAdapter implements RoleModel, JpaModel<RoleEntity> {
protected RoleEntity role;
protected EntityManager em;
protected RealmModel realm;
@ -46,7 +47,7 @@ public class RoleAdapter implements RoleModel {
this.session = session;
}
public RoleEntity getRole() {
public RoleEntity getEntity() {
return role;
}
@ -97,17 +98,17 @@ public class RoleAdapter implements RoleModel {
@Override
public void addCompositeRole(RoleModel role) {
RoleEntity entity = RoleAdapter.toRoleEntity(role, em);
for (RoleEntity composite : getRole().getCompositeRoles()) {
for (RoleEntity composite : getEntity().getCompositeRoles()) {
if (composite.equals(entity)) return;
}
getRole().getCompositeRoles().add(entity);
getEntity().getCompositeRoles().add(entity);
em.flush();
}
@Override
public void removeCompositeRole(RoleModel role) {
RoleEntity entity = RoleAdapter.toRoleEntity(role, em);
Iterator<RoleEntity> it = getRole().getCompositeRoles().iterator();
Iterator<RoleEntity> it = getEntity().getCompositeRoles().iterator();
while (it.hasNext()) {
if (it.next().equals(entity)) it.remove();
}
@ -117,7 +118,7 @@ public class RoleAdapter implements RoleModel {
public Set<RoleModel> getComposites() {
Set<RoleModel> set = new HashSet<RoleModel>();
for (RoleEntity composite : getRole().getCompositeRoles()) {
for (RoleEntity composite : getEntity().getCompositeRoles()) {
set.add(new RoleAdapter(session, realm, em, composite));
// todo I want to do this, but can't as you get stack overflow
@ -161,7 +162,7 @@ public class RoleAdapter implements RoleModel {
public static RoleEntity toRoleEntity(RoleModel model, EntityManager em) {
if (model instanceof RoleAdapter) {
return ((RoleAdapter)model).getRole();
return ((RoleAdapter)model).getEntity();
}
return em.getReference(RoleEntity.class, model.getId());
}

View file

@ -63,7 +63,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UserAdapter implements UserModel {
public class UserAdapter implements UserModel, JpaModel<UserEntity> {
protected UserEntity user;
protected EntityManager em;
@ -77,7 +77,7 @@ public class UserAdapter implements UserModel {
this.session = session;
}
public UserEntity getUser() {
public UserEntity getEntity() {
return user;
}
@ -503,7 +503,7 @@ public class UserAdapter implements UserModel {
// we query ids only as the group might be cached and following the @ManyToOne will result in a load
// even if we're getting just the id.
TypedQuery<String> query = em.createNamedQuery("userGroupIds", String.class);
query.setParameter("user", getUser());
query.setParameter("user", getEntity());
List<String> ids = query.getResultList();
Set<GroupModel> groups = new HashSet<>();
for (String groupId : ids) {
@ -518,7 +518,7 @@ public class UserAdapter implements UserModel {
public void joinGroup(GroupModel group) {
if (isMemberOf(group)) return;
UserGroupMembershipEntity entity = new UserGroupMembershipEntity();
entity.setUser(getUser());
entity.setUser(getEntity());
entity.setGroupId(group.getId());
em.persist(entity);
em.flush();
@ -548,7 +548,7 @@ public class UserAdapter implements UserModel {
protected TypedQuery<UserGroupMembershipEntity> getUserGroupMappingQuery(GroupModel group) {
TypedQuery<UserGroupMembershipEntity> query = em.createNamedQuery("userMemberOf", UserGroupMembershipEntity.class);
query.setParameter("user", getUser());
query.setParameter("user", getEntity());
query.setParameter("groupId", group.getId());
return query;
}
@ -562,7 +562,7 @@ public class UserAdapter implements UserModel {
protected TypedQuery<UserRoleMappingEntity> getUserRoleMappingEntityTypedQuery(RoleModel role) {
TypedQuery<UserRoleMappingEntity> query = em.createNamedQuery("userHasRole", UserRoleMappingEntity.class);
query.setParameter("user", getUser());
query.setParameter("user", getEntity());
query.setParameter("roleId", role.getId());
return query;
}
@ -571,7 +571,7 @@ public class UserAdapter implements UserModel {
public void grantRole(RoleModel role) {
if (hasRole(role)) return;
UserRoleMappingEntity entity = new UserRoleMappingEntity();
entity.setUser(getUser());
entity.setUser(getEntity());
entity.setRoleId(role.getId());
em.persist(entity);
em.flush();
@ -598,7 +598,7 @@ public class UserAdapter implements UserModel {
// we query ids only as the role might be cached and following the @ManyToOne will result in a load
// even if we're getting just the id.
TypedQuery<String> query = em.createNamedQuery("userRoleMappingIds", String.class);
query.setParameter("user", getUser());
query.setParameter("user", getEntity());
List<String> ids = query.getResultList();
Set<RoleModel> roles = new HashSet<RoleModel>();
for (String roleId : ids) {

View file

@ -50,6 +50,7 @@ import java.util.Set;
@Table(name="CLIENT", uniqueConstraints = {@UniqueConstraint(columnNames = {"REALM_ID", "CLIENT_ID"})})
@NamedQueries({
@NamedQuery(name="getClientsByRealm", query="select client from ClientEntity client where client.realm = :realm"),
@NamedQuery(name="getClientById", query="select client from ClientEntity client where client.id = :id and client.realm.id = :realm"),
@NamedQuery(name="getClientIdsByRealm", query="select client.id from ClientEntity client where client.realm.id = :realm"),
@NamedQuery(name="findClientIdByClientId", query="select client.id from ClientEntity client where client.clientId = :clientId and client.realm.id = :realm"),
@NamedQuery(name="findClientByClientId", query="select client from ClientEntity client where client.clientId = :clientId and client.realm.id = :realm"),

View file

@ -36,7 +36,6 @@ import org.keycloak.models.utils.KeycloakModelUtils;
public class ConcurrentTransactionsTest extends AbstractModelTest {
@Test
@Ignore
public void persistClient() throws Exception {
RealmModel realm = realmManager.createRealm("original");
KeycloakSession session = realmManager.getSession();
@ -129,6 +128,8 @@ public class ConcurrentTransactionsTest extends AbstractModelTest {
thread1.join();
thread2.join();
System.out.println("after thread join");
commit();
session = realmManager.getSession();