Differences between revisions 1 and 2
Revision 1 as of 2008-01-19 18:59:22
Size: 6939
Editor: ChrisLewis
Comment: How to implement entity timestamping with Hibernate using tapestry-hibernate.
Revision 2 as of 2009-09-20 23:20:10
Size: 6959
Editor: localhost
Comment: converted to 1.6 markup
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
If you've used [http://www.rubyonrails.org/ RoR] at all, or a knock-off such as [http://cakephp.org/ CakePHP], you may have found it's auto-magic time stamping quite useful. It works like this - if your model/domain object/entity defines a property named 'created', then every time a new object of that type is saved that field will be populated with the current timestamp in the resulting database row. Similarly, if the entity defines a 'modified' property then each time an existing row is updated the property will be set to the time of updating. I found this feature to be very useful, and so sought out how to do it using [http://tapestry.apache.org/tapestry5/tapestry-hibernate/index.html tapestry-hibernate] in a Tapestry 5 application. If you've used [[http://www.rubyonrails.org/|RoR]] at all, or a knock-off such as [[http://cakephp.org/|CakePHP]], you may have found it's auto-magic time stamping quite useful. It works like this - if your model/domain object/entity defines a property named 'created', then every time a new object of that type is saved that field will be populated with the current timestamp in the resulting database row. Similarly, if the entity defines a 'modified' property then each time an existing row is updated the property will be set to the time of updating. I found this feature to be very useful, and so sought out how to do it using [[http://tapestry.apache.org/tapestry5/tapestry-hibernate/index.html|tapestry-hibernate]] in a Tapestry 5 application.
Line 6: Line 6:
To automatically have fields populated by Java code (so we don't rely on database-specific triggers), I assumed that hibernate probably exposed a kind of event system. As it turns out I was right, and the events system is robust and fully capable of doing this. However I chose the [http://www.hibernate.org/hib_docs/v3/api/org/hibernate/Interceptor.html Interceptor] method. Following the advice shared in [http://www.google.com/search?client=safari&rls=it-it&q=java+persistence+with+hibernate&ie=UTF-8&oe=UTF-8 Java Persistence With Hibernate], I extended the [http://www.hibernate.org/hib_docs/v3/api/org/hibernate/EmptyInterceptor.html EmptyInterceptor] so I could simply override the methods I needed; namely, `onSave` and `isTransient`. Following is my simple implementation: To automatically have fields populated by Java code (so we don't rely on database-specific triggers), I assumed that hibernate probably exposed a kind of event system. As it turns out I was right, and the events system is robust and fully capable of doing this. However I chose the [[http://www.hibernate.org/hib_docs/v3/api/org/hibernate/Interceptor.html|Interceptor]] method. Following the advice shared in [[http://www.google.com/search?client=safari&rls=it-it&q=java+persistence+with+hibernate&ie=UTF-8&oe=UTF-8|Java Persistence With Hibernate]], I extended the [[http://www.hibernate.org/hib_docs/v3/api/org/hibernate/EmptyInterceptor.html|EmptyInterceptor]] so I could simply override the methods I needed; namely, `onSave` and `isTransient`. Following is my simple implementation:
Line 87: Line 87:
This is just a simple [http://www.hibernate.org/hib_docs/v3/api/org/hibernate/Interceptor.html Interceptor], it has nothing to do with Tapestry and could be used in any hibernate application. Looking at the `onSave` method, you can see that it attempts to update the fields `created` and `modified` of the entity being saved. The `modified` field is updated with the current date anytime the entity is saved, while the `created` field is set only when the entity is first saved. Note that no reflection, annotations, or interfaces are used here. If the entity has either of these fields, they will be updated according to this logic. Of course if you wish to be more restrictive or precise you can do that as well. Perhaps you want to change the field names, or maybe you want to use an annotation or explicit interface. That part would be easy to implement, so I leave that to you. This is just a simple [[http://www.hibernate.org/hib_docs/v3/api/org/hibernate/Interceptor.html|Interceptor]], it has nothing to do with Tapestry and could be used in any hibernate application. Looking at the `onSave` method, you can see that it attempts to update the fields `created` and `modified` of the entity being saved. The `modified` field is updated with the current date anytime the entity is saved, while the `created` field is set only when the entity is first saved. Note that no reflection, annotations, or interfaces are used here. If the entity has either of these fields, they will be updated according to this logic. Of course if you wish to be more restrictive or precise you can do that as well. Perhaps you want to change the field names, or maybe you want to use an annotation or explicit interface. That part would be easy to implement, so I leave that to you.
Line 91: Line 91:
Before proceeding it may be helpful to check out the official [http://tapestry.apache.org/tapestry5/tapestry-hibernate/conf.html configuration documentation] for tapestry-hibernate. All done? Good, moving on. You'll have noticed that the module does a little indirection by having you provide an implementation of [http://tapestry.apache.org/tapestry5/apidocs/org/apache/tapestry/hibernate/HibernateConfigurer.html HibernateConfigurer], which has the single method `configure`. As an argument to this method, you get a reference to the [http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cfg/Configuration.html Configuration] object of the current session, which we need to attach our interceptor. The `HibernateConfigurer ` is skinny, so we'll use an anonymous implementation directly in our app module: Before proceeding it may be helpful to check out the official [[http://tapestry.apache.org/tapestry5/tapestry-hibernate/conf.html|configuration documentation]] for tapestry-hibernate. All done? Good, moving on. You'll have noticed that the module does a little indirection by having you provide an implementation of [[http://tapestry.apache.org/tapestry5/apidocs/org/apache/tapestry/hibernate/HibernateConfigurer.html|HibernateConfigurer]], which has the single method `configure`. As an argument to this method, you get a reference to the [[http://www.hibernate.org/hib_docs/v3/api/org/hibernate/cfg/Configuration.html|Configuration]] object of the current session, which we need to attach our interceptor. The `HibernateConfigurer ` is skinny, so we'll use an anonymous implementation directly in our app module:

If you've used RoR at all, or a knock-off such as CakePHP, you may have found it's auto-magic time stamping quite useful. It works like this - if your model/domain object/entity defines a property named 'created', then every time a new object of that type is saved that field will be populated with the current timestamp in the resulting database row. Similarly, if the entity defines a 'modified' property then each time an existing row is updated the property will be set to the time of updating. I found this feature to be very useful, and so sought out how to do it using tapestry-hibernate in a Tapestry 5 application.

1) The Hibernate Part

I am not a hibernate expert by any means, and my knowledge of what I am sharing comes directly from researching the different ways in which this could be accomplished.

To automatically have fields populated by Java code (so we don't rely on database-specific triggers), I assumed that hibernate probably exposed a kind of event system. As it turns out I was right, and the events system is robust and fully capable of doing this. However I chose the Interceptor method. Following the advice shared in Java Persistence With Hibernate, I extended the EmptyInterceptor so I could simply override the methods I needed; namely, onSave and isTransient. Following is my simple implementation:

import java.io.Serializable;
import java.util.Date;

import org.hibernate.EmptyInterceptor;
import org.hibernate.Session;
import org.hibernate.TransientObjectException;
import org.hibernate.type.Type;
import org.slf4j.Logger;

/**
 * EntityInterceptor
 * 
 * @author Chris Lewis 19/gen/08 <chris@thegodcode.net>
 * @version $Id: EntityInterceptor.java 25 2008-01-19 18:00:09Z burningodzilla $
 */
public class EntityInterceptor extends EmptyInterceptor {
        
        private Logger log;
        private Session session;
        
        public EntityInterceptor(Session session, Logger log) {
                this.session = session;
                this.log = log;
        }
        
        public boolean onSave(
                        Object entity, 
                        Serializable id, 
                        Object[] state, 
                        String[] propertyNames, 
                        Type[] types) {
                
                boolean modified = false;
                Date dateModified = new Date();
                if(isTransient(entity)) {
                        modified = modifyProperty("created", dateModified, state, propertyNames);
                }
                
                return modifyProperty("modified", dateModified, state, propertyNames) || modified;
        }
        
        public Boolean isTransient(Object entity) {
                /*
                 * Our algorithm for transience is extremely general. If the entity identifier
                 * is null, then we assume it is transient. 
                 */
                try {
                        return this.session.getIdentifier(entity) == null;
                } catch (TransientObjectException e) {
                        return true;
                }
                
        }
        
        /**
         * Modify a property in an entity state array.
         * @param prop the property name to modify
         * @param value the value to assign
         * @param state the current entity state array
         * @param propertyNames the current entity property array
         * @return <code>true</code> if a modification was made, <code>false</code> if not
         */
        protected boolean modifyProperty(String prop, Object value, Object[] state, String[] propertyNames) {
                boolean modified = false;
                for(int i = 0; i < propertyNames.length; i++) {
                        if(propertyNames[i].equals(prop)) {
                                if(state[i] != value) {
                                        state[i] = value;
                                        modified = true;
                                }
                        }
                }
                return modified;
        }
        
}

This is just a simple Interceptor, it has nothing to do with Tapestry and could be used in any hibernate application. Looking at the onSave method, you can see that it attempts to update the fields created and modified of the entity being saved. The modified field is updated with the current date anytime the entity is saved, while the created field is set only when the entity is first saved. Note that no reflection, annotations, or interfaces are used here. If the entity has either of these fields, they will be updated according to this logic. Of course if you wish to be more restrictive or precise you can do that as well. Perhaps you want to change the field names, or maybe you want to use an annotation or explicit interface. That part would be easy to implement, so I leave that to you.

2) Wiring up the Interceptor via Tapestry IoC

Before proceeding it may be helpful to check out the official configuration documentation for tapestry-hibernate. All done? Good, moving on. You'll have noticed that the module does a little indirection by having you provide an implementation of HibernateConfigurer, which has the single method configure. As an argument to this method, you get a reference to the Configuration object of the current session, which we need to attach our interceptor. The HibernateConfigurer  is skinny, so we'll use an anonymous implementation directly in our app module:

        public static void contributeHibernateSessionSource(
                OrderedConfiguration<HibernateConfigurer> config,
                final Session session) {
                
                config.add("HibernateConfiguration", new HibernateConfigurer() {
                        public void configure(Configuration configuration) {
                                /*
                                 * I'm having trouble getting a reference to an implementation at the moment,
                                 * so we'll settle on this for the moment.
                                 */
                                configuration.setInterceptor(new EntityInterceptor(session,
                                        LoggerFactory.getLogger(EntityInterceptor.class)));
                        }
                });
        }

With this now wired up you can simply add created and modified fields (of type java.util.Date) to your entity class, and now the corresponding rows will auto-magically have these times updated for you.

Conclusion

Obviously there are a handful of things to consider, including things like if you want a more strict system with annotations or interfaces marking the entity classes to be handled or what the fields should be called. You may also want to use a different data type for the field, and you may even prefer to use hibernate's events system instead of an interceptor. That is all up to you as the developer, this is simply an example of how you can be a little lazier ;-).

Thanks to TomTom in #tapestry on irc.freenode.net (IRC) for the chats on interceptors - they were most helpful.

Tapestry5BrainlessEntityTimestamping (last edited 2009-09-20 23:20:10 by localhost)