Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update indexes when associated entities are modified #139

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -128,9 +128,9 @@ class AuditEventListener extends AbstractPersistenceEventListener {
logger.warn('Received a PostInsertEvent with no entity')
return
}
if (elasticSearchContextHolder.isRootClass(entity.class)) {
pushToIndex(entity)
}

Set roots = getRootIndexedEntity(entity)
roots?.each { pushToIndex(it) }
}

void onPostUpdate(PostUpdateEvent event) {
Expand All @@ -139,9 +139,9 @@ class AuditEventListener extends AbstractPersistenceEventListener {
logger.warn('Received a PostUpdateEvent with no entity')
return
}
if (elasticSearchContextHolder.isRootClass(entity.class)) {
pushToIndex(entity)
}

Set roots = getRootIndexedEntity(entity)
roots?.each { pushToIndex(it) }
}

void onPostDelete(PostDeleteEvent event) {
Expand All @@ -150,9 +150,9 @@ class AuditEventListener extends AbstractPersistenceEventListener {
logger.warn('Received a PostDeleteEvent with no entity')
return
}
if (elasticSearchContextHolder.isRootClass(entity.class)) {
pushToDelete(entity)
}

Set roots = getRootIndexedEntity(entity)
roots?.each { pushToDelete(it) }
}

Map getPendingObjects() {
Expand All @@ -171,6 +171,46 @@ class AuditEventListener extends AbstractPersistenceEventListener {
deletedObjects.remove()
}

/**
* Recursively traverse up the entity hierarchy, using the <code>belongsTo</code> property to find the parent(s) of
* the entity being modified.
* <p/>
*
* As long as each parent is 'searchable' (i.e. it has the 'searchable' static member), the code will continue up
* the hierarchy until it finds the root elements for the search index.
* <p/>
*
* Note: this relies on the entity being updated having a back reference to its parent(s).
*
* @param entity the entity being modified
* @return Set of zero or more root indexed entities
*/
Set getRootIndexedEntity(entity) {
Set roots = []

if (elasticSearchContextHolder.isRootClass(entity.class)) {
roots << entity
} else if (entity.hasProperty('searchable') && entity.searchable && entity.hasProperty('belongsTo')) {
if (entity.belongsTo instanceof Map) {
entity.belongsTo.keySet().each {
def parent = entity[it]

roots.addAll(getRootIndexedEntity(parent))
}
} else if (entity.belongsTo instanceof List) {
entity.belongsTo.each { parentType ->
def parent = entity.class.getDeclaredFields().find { it.getType() == parentType }

if (parent) {
roots.addAll(getRootIndexedEntity(parent))
}
}
}
}

roots
}

private def getEventEntity(AbstractPersistenceEvent event) {
if (event.entityAccess) {
return event.entityAccess.entity
Expand Down
@@ -0,0 +1,133 @@
package org.grails.plugins.elasticsearch

import spock.lang.Specification

class AuditEventListenerRootIndexSpec extends Specification {

AuditEventListener listener = new AuditEventListener(null)

def setup() {
listener.elasticSearchContextHolder = Mock(ElasticSearchContextHolder)
listener.elasticSearchContextHolder.isRootClass(_) >> { List args ->
return args[0] == IndexRootA || args[0] == IndexRootB
}
}

def "an entity with root = true should be returned"() {
given: "an instance of an entity that is the root of the search index"
IndexRootA a = new IndexRootA()

when: "asked for the root of the search index"
Set roots = listener.getRootIndexedEntity(a)

then: "a should be returned since it is the index root"
roots == [a] as Set
}

def "an entity with no searchable property should result in an empty list"() {
given: "an instance of an entity that is not searchable"
NoParent g = new NoParent()

when: "asked for the root of the search index"
Set roots = listener.getRootIndexedEntity(g)

then: "an empty list should be returned since the entity is not searchable"
roots.isEmpty()
}

def "an entity with no back reference to the parent should result in an empty list"() {
given: "an instance of an entity with no back reference to its parent"
NoBackReference h = new NoBackReference()

when: "asked for the root of the search index"
Set roots = listener.getRootIndexedEntity(h)

then: "an empty list should be returned since the entity does not have a back reference to its parent"
roots.isEmpty()
}

def "a searchable entity whose parent is the root should result in a list containing the parent"() {
given: "an instance of a searchable entity whose parent is the index root"
ParentIsRoot d = new ParentIsRoot(a: new IndexRootA())

when: "asked for the root of the search index"
Set roots = listener.getRootIndexedEntity(d)

then: "a single item list should be returned containing the parent entity"
roots[0] == d.a
}

def "a searchable entity with two parents which are index roots should result in a list containing both parents"() {
given: "an instance of a searchable entity with two parents which are index roots"
TwoParents c = new TwoParents(a: new IndexRootA(), b: new IndexRootB())

when: "asked for the root of the search index"
Set roots = listener.getRootIndexedEntity(c)

then: "a list containing both parents should be returned"
roots[0] == c.a
roots[1] == c.b
}

def "a searchable entity whose grandparent is the index root should result in a list containing the grandparent"() {
given: "an instance of a searchable entity whose grandparent is the index root"
GrandParentIsRoot e = new GrandParentIsRoot(d: new ParentIsRoot(a: new IndexRootA()))

when: "asked for the root of the search index"
Set roots = listener.getRootIndexedEntity(e)

then: "a single item list should be returned containing the grandparent entity"
roots[0] == e.d.a
}

def "a searchable entity whose parent is not searchable should result in an empty list"() {
given: "an instance of a searchable entity whose parent is not searchable"
NonSearchableParent i = new NonSearchableParent(h: new NoParent())

when: "asked for the root of the search index"
Set roots = listener.getRootIndexedEntity(i)

then: "an empty list should be returned because the entity is not the root and its parent is not searchable"
roots.isEmpty()
}

}

class IndexRootA {
static searchable = { root = true }
}

class IndexRootB {
static searchable = true
}

class TwoParents {
static searchable = { root = false }
IndexRootA a
IndexRootB b
static belongsTo = [a: IndexRootA, b: IndexRootB]
}

class ParentIsRoot {
static searchable = { root = false }
IndexRootA a
static belongsTo = [a: IndexRootA]
}

class GrandParentIsRoot {
static searchable = { root = false }
ParentIsRoot d
static belongsTo = [d: ParentIsRoot]
}

class NoParent {}

class NoBackReference {
static belongsTo = [IndexRootA]
}

class NonSearchableParent {
static searchable = { root = false }
NoParent h
static belongsTo = [NoParent]
}