简体   繁体   中英

Grails custom listener, updating property generates other event?

I need to calculate coords from some address fields in a Client domain, so in order to decouple the domain class from this requirement I 've added a custom listener that uses Google Maps API to get the right values.

Everything works, but debuggin' I've realized that updating domain properties inside the listener lauches another updating event and consecuently my listener calls the Google API twice for each update.

Someone experiencing this issue?? What am I doing wrong??

Domain:

class Client{
  String address
  String city
  String country
  String postalCode
  double lat
  double lng
  ....
}

Service:

class GoogleMapsService{
      static transactional=false
      def grailsApplication

      def geocode(String address){
        address=address.replaceAll(" ", "+")
        def urlApi=grailsApplication.config.googleMaps.apiUrl+"&address=${address}"     
        def urlJSON = new URL(urlApi)
        def geoCodeResultJSON = new JsonSlurper().parseText(urlJSON.getText())              
        return geoCodeResultJSON            
      }
}

Event listener:

class ClientListener extends AbstractPersistenceEventListener{
public boolean supportsEventType(Class<? extends ApplicationEvent> eventClass) {
    switch(eventClass){
        case [PreInsertEvent,
                        PreUpdateEvent, 
                        PostInsertEvent,
                        PostUpdateEvent,
                        PostDeleteEvent]:
            return true
        default:
            return false
    }
}        

protected void onPersistenceEvent(AbstractPersistenceEvent event) {
    if(!(event.entityObject instanceof Client)){
        return
    }

    switch(event.eventType) {
        //GOOGLE MAPS geocode
        case [EventType.PreInsert,EventType.PreUpdate]:
            this.updateCoords(event.entityObject)
            break

        //OTHER STUFF (notifications, no changing inside)
        case EventType.PostUpdate:
            //....
    }
}

private String composeAddress(Client cli){
    def resul=[cli.address,cli.city,cli.country,cli.postalCode]     
    return resul.findAll{it}.join(",")          
}

private void updateCoords(Client cli){
    def fullAddress=this.composeAddress(cli)
    if(fullAddress){
        def coords=googleMapsService.geocode(fullAddress)
        if (coords.status=="OK"){
            //**IMPORTANT STUFF THESE TWO LINES RAISE AN EVENT
            cli.lat=coords.results.geometry.location.lat[0]
            cli.lng=coords.results.geometry.location.lng[0]
        }
    }
}        
}

Update:

Entity is null inside the listener so I cant make it work from preUpdate, Looks like there is an open issue ( http://jira.grails.org/browse/GRAILS-9374 )

Gonna try the SaveOrUpdate...


Big Update:

I'm getting closer but still cant avoid the duplicated call.

Since I need a 'saveOrUpdate' listener and this one is a Hibernate specific listener I end up having two listeners:

  • ClientMailListener (Grails custom listener, postinsert, postupdate, postdelete with no changes in the object)
  • ClientGeoListener (Hibernate listener, mapped as 'save-update')

The status with this combo is:

  • Update: OK, both listeners are called just once
  • Insert: KO!!, let's have deeper look.

1. It does an insert and calls the postInsert grails listener, Why????

ClientMailListener.onPersistenceEvent(AbstractPersistenceEvent) line: 45    
ClientMailListener(AbstractPersistenceEventListener).onApplicationEvent(ApplicationEvent) line: 46  
SimpleApplicationEventMulticaster.multicastEvent(ApplicationEvent) line: 97 
GrailsWebApplicationContext(AbstractApplicationContext).publishEvent(ApplicationEvent) line: 324    
ClosureEventTriggeringInterceptor.publishEvent(AbstractEvent, AbstractPersistenceEvent) line: 163   
ClosureEventTriggeringInterceptor.onPostInsert(PostInsertEvent) line: 129   
EntityIdentityInsertAction.postInsert() line: 131   
EntityIdentityInsertAction.execute() line: 90   
ActionQueue.execute(Executable) line: 273   
ClosureEventTriggeringInterceptor.performSaveOrReplicate(Object, EntityKey, EntityPersister, boolean, Object, EventSource, boolean) line: 250   
ClosureEventTriggeringInterceptor(AbstractSaveEventListener).performSave(Object, Serializable, EntityPersister, boolean, Object, EventSource, boolean) line: 203    
ClosureEventTriggeringInterceptor(AbstractSaveEventListener).saveWithGeneratedId(Object, String, Object, EventSource, boolean) line: 129    
ClosureEventTriggeringInterceptor(DefaultSaveOrUpdateEventListener).saveWithGeneratedOrRequestedId(SaveOrUpdateEvent) line: 210 
ClosureEventTriggeringInterceptor(DefaultSaveOrUpdateEventListener).entityIsTransient(SaveOrUpdateEvent) line: 195  
ClosureEventTriggeringInterceptor(DefaultSaveOrUpdateEventListener).performSaveOrUpdate(SaveOrUpdateEvent) line: 117    
ClosureEventTriggeringInterceptor(DefaultSaveOrUpdateEventListener).onSaveOrUpdate(SaveOrUpdateEvent) line: 93  
ClosureEventTriggeringInterceptor.onSaveOrUpdate(SaveOrUpdateEvent) line: 108   
SessionImpl.fireSaveOrUpdate(SaveOrUpdateEvent) line: 685   

2. Then calls the save-update listener, and updates de object

ClientGeoListener.onSaveOrUpdate(SaveOrUpdateEvent) line: 34    
SessionImpl.fireSaveOrUpdate(SaveOrUpdateEvent) line: 685   
SessionImpl.saveOrUpdate(String, Object) line: 677  
SessionImpl.saveOrUpdate(Object) line: 673  

3. Consecuently the postUpdate grails listener is called again, :(


Last Update

Finally try with just one listener to keep it simple. Looks like the whole point is that the hibernate 'save-update' listener is executed after the insert, details:

If I disabled the custom listener leaving the Hibernate listener (GEO) this is what happens:

1. Insert into the client (blank coords) and calls the saveupdate Listener:

ClientGeoListener.onSaveOrUpdate(SaveOrUpdateEvent) line: 34    
SessionImpl.fireSaveOrUpdate(SaveOrUpdateEvent) line: 685   
SessionImpl.saveOrUpdate(String, Object) line: 677  

2. Coords are updated and there is an update

Sorry for this large update, Ideas??

Grails is using Hibernate in the background. Typically you don't update properties inside the PreUpdate event. You can do it, but if you don't update both the property and the underlying PersistentEntity you will probably end up with some undesired behavior.

The workflow which explains why you're getting two updates:

  1. You make changes to your domain object, and the time comes to persist the event.
  2. Hibernate decides what to persist.
  3. BeforeUpdate fires, updates lat / lng properties.
  4. Hibernate persists the object, but with the old values for lat / lng .
  5. Domain object is now dirty
  6. Hibernate decides it needs to persist the dirty object (now with the correct values).
  7. BeforeUpdate fires, gets same value for lat / lng from API.
  8. Hibernate persists the object with the new values.
  9. The domain object and the persistent object now match. Hibernate is happy.

The simplest solution to avoid a double update would be to use the SaveOrUpdate event instead of BeforeUpdate because that actually happens earlier in the process.

However, since your goal is to minimize the number of unnecessary API calls it's worth still using BeforeUpdate because manually checkig your entity for dirtiness before calling the API is probably more effort than it's worth. So, what you have to do is, as @Andrew suggested, just make sure you update the property and the entity, ie:

event.entity.setProperty('lat', coords.results.geometry.location.lat[0])
event.entity.setProperty('lng', coords.results.geometry.location.lng[0])
cli.lat = coords.results.geometry.location.lat[0]
cli.lng = coords.results.geometry.location.lng[0] 

Update: My assumption that checking whether the domain object is dirty might be annoying is actually based on code I had to write for NHibernate. This being Grails of course, it should be as simple as this:

if (event.entityObject.isDirty()) { /* Call API */ }

Instead of direct lat / lng setter access, perhaps try using EntityAccess . See the AutoTimestamp listener in Grails core for an example.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM