Using Grails spring security REST(which in itself uses Grails Spring Security Core) I've generated User, Role, UserRole classes.
User:
class User extends DomainBase{
transient springSecurityService
String username
String password
String firstName
String lastNameOrTitle
String email
boolean showEmail
String phoneNumber
boolean enabled = true
boolean accountExpired
boolean accountLocked
boolean passwordExpired
static transients = ['springSecurityService']
static hasMany = [
roles: Role,
ratings: Rating,
favorites: Favorite
]
static constraints = {
username blank: false, unique: true
password blank: false
firstName nullable: true, blank: false
lastNameOrTitle nullable: false, blank: false
email nullable: false, blank: false
phoneNumber nullable: true
}
static mapping = {
DomainUtil.inheritDomainMappingFrom(DomainBase, delegate)
id column: 'user_id', generator: 'sequence', params: [sequence: 'user_seq']
username column: 'username'
password column: 'password'
enabled column: 'enabled'
accountExpired column: 'account_expired'
accountLocked column: 'account_locked'
passwordExpired column: 'password_expired'
roles joinTable: [
name: 'user_role',
column: 'role_id',
key: 'user_id']
}
Set<Role> getAuthorities() {
// UserRole.findAllByUser(this).collect { it.role }
// userRoles.collect { it.role }
this.roles
}
def beforeInsert() {
encodePassword()
}
def beforeUpdate() {
super.beforeUpdate()
if (isDirty('password')) {
encodePassword()
}
}
protected void encodePassword() {
password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
}
}
Role:
class Role {
String authority
static mapping = {
cache true
id column: 'role_id', generator: 'sequence', params: [sequence: 'role_seq']
authority column: 'authority'
}
static constraints = {
authority blank: false, unique: true
}
}
UserRole:
class UserRole implements Serializable {
private static final long serialVersionUID = 1
static belongsTo = [
user: User,
role: Role
]
// User user
// Role role
boolean equals(other) {
if (!(other instanceof UserRole)) {
return false
}
other.user?.id == user?.id &&
other.role?.id == role?.id
}
int hashCode() {
def builder = new HashCodeBuilder()
if (user) builder.append(user.id)
if (role) builder.append(role.id)
builder.toHashCode()
}
static UserRole get(long userId, long roleId) {
UserRole.where {
user == User.load(userId) &&
role == Role.load(roleId)
}.get()
}
static boolean exists(long userId, long roleId) {
UserRole.where {
user == User.load(userId) &&
role == Role.load(roleId)
}.count() > 0
}
static UserRole create(User user, Role role, boolean flush = false) {
def instance = new UserRole(user: user, role: role)
instance.save(flush: flush, insert: true)
instance
}
static boolean remove(User u, Role r, boolean flush = false) {
if (u == null || r == null) return false
int rowCount = UserRole.where {
user == User.load(u.id) &&
role == Role.load(r.id)
}.deleteAll()
if (flush) {
UserRole.withSession { it.flush() }
}
rowCount > 0
}
static void removeAll(User u, boolean flush = false) {
if (u == null) return
UserRole.where {
user == User.load(u.id)
}.deleteAll()
if (flush) {
UserRole.withSession { it.flush() }
}
}
static void removeAll(Role r, boolean flush = false) {
if (r == null) return
UserRole.where {
role == Role.load(r.id)
}.deleteAll()
if (flush) {
UserRole.withSession { it.flush() }
}
}
static constraints = {
role validator: { Role r, UserRole ur ->
if (ur.user == null) return
boolean existing = false
UserRole.withNewSession {
existing = UserRole.exists(ur.user.id, r.id)
}
if (existing) {
return 'userRole.exists'
}
}
}
static mapping = {
id composite: ['role', 'user']
version false
}
}
Now I wish to create an admin area where admins can modify/enable user accounts, but can't touch other admins, so for that I've decided to create a pageable query which would select only the users which don't have the ROLE_ADMIN role, since admins have both ROLE_USER and ROLE_ADMIN roles.
As can be seen from the above code, I've modified the default generated code a bit and added a joinTable to the User class instead of hasMany: [roles:UserRole] or keeping it at the default without any references to roles. The reason for this change is because when querying UserRole I'd occasionally get duplicates which would make pagination difficult.
So with this current setup I've managed to create two queries which allow me to fetch only users which do not have an admin role.
def rolesToIgnore = ["ROLE_ADMIN"]
def userIdsWithGivenRoles = User.createCriteria().list() {
projections {
property "id"
}
roles {
'in' "authority", rolesToIgnore
}
}
def usersWithoutGivenRoles = User.createCriteria().list(max: 10, offset: 0) {
not {
'in' "id", userIdsWithGivenRoles
}
}
First query fetches a list of all the user ids which have the ROLE_ADMIN role, and then the second query fetches all the users whose id is not in the previous list.
This works and is pageable, however It bothers me for two reasons:
joinTableon User just seems "icky" to me. Why use ajoinTablewhen i already have a specific class for that purposeUserRole, however that class is more difficult to query and I'm afraid of possible overhead of mappingRolefor each foundUsereven though I only need theUser.- Two queries, and only the second one can be paged.
So my questions are: Is there a more optimal way to construct a query for fetching users which don't contain certain roles (without restructuring the database into a pyramid role system where every user has only one role)?
Are two queries absolutely necessary? I've tried to construct a pure SQL query and I couldn't do it without subqueries.