mandubian/siena — Issue #15: Lifecycle annotations: @PreUpdate and @PreInsert not called on save()
Author: henricpersson
Created: 2011-12-27 10:39:35+00:00
State: open

We often want to do stuff before an entity is stored in the database. A common case is that we want to keep track of the timestamp when an entity was last updated. For this, we have previously been overriding the insert() method:

```java
@Override
public void insert() {
lastUpdated = new Date();
super.insert();
}
```

This is usually enough for our purposes, even though it's not pretty. One thing though that bothers us though is that we're never 100% sure that the method will actually be called on insert. If we call save() instead of insert(), our code is never executed. Also, if we make a batch insert through Model.insert(), our code is never executed.

We'd been thinking about adding annotations as a way of doing this instead of overriding. Then we came across the Lifecycle support which looked exactly like what we wanted. But it doesn't behave the way we though it would. Most importantly: why is there a need for a @PreSave annotation? **save()** is a handy way of inserting or updating an entity without caring whether it has been inserted before. But it should behave as if you would either call **insert()** or **update()**. With the current implementation, we have to tell developers not to use save() for the side effects they may cause.

Below is a test case demonstrating what's wrong (same test case as for the other lifecycle bug I added). For this issue, look at the **preUpdateAndPreInsertShouldBeCalledOnSave()** method.

```java
package siena.lifecycle.test;

import java.util.Date;

import junit.framework.Assert;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import siena.Generator;
import siena.Id;
import siena.Model;
import siena.PersistenceManager;
import siena.PersistenceManagerFactory;
import siena.core.PersistenceManagerLifeCycleWrapper;
import siena.core.lifecycle.PreInsert;
import siena.core.lifecycle.PreUpdate;
import siena.gae.GaePersistenceManager;

import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig;

public class LifeCycleTest {

/**
* A simple model class with lifecycle annotations
*
* @author henper
*
*/
public static class LifeCycleEntity extends Model {

@Id(Generator.AUTO_INCREMENT)
public Long id;
public Date created;
public Date updated;

public @PreInsert void onInsert() {
created = new Date();
}

public @PreUpdate @PreInsert void onUpdate() {
updated = new Date();
}
}

@Test
public void preInsertShouldWorkOnBatchInsert() {
// If you are storing multiple entities at once, a batch insert is handy:
LifeCycleEntity one = new LifeCycleEntity();
LifeCycleEntity two = new LifeCycleEntity();
Model.batch(LifeCycleEntity.class).insert(one, two);

// It is expected that the @PreInsert is called once for each entity, before inserting them:
Assert.assertNotNull("The \"created\" date was not updated for entity \"one\"", one.created);
Assert.assertNotNull("The \"created\" date was not updated for entity \"two\"", two.created);
}

@Test
public void preUpdateAndPreInsertShouldBeCalledOnSave() {
LifeCycleEntity one = new LifeCycleEntity();
one.save();
Assert.assertNotNull("The \"created\" date was not updated on save()", one.created);
Assert.assertNotNull("The \"updated\" date was not updated on save()", one.updated);
}

@Test
public void preInsertShouldBeCalledOnInsert() {
LifeCycleEntity one = new LifeCycleEntity();
one.insert();
Assert.assertNotNull("The \"created\" date was not updated on insert()", one.created);
Assert.assertNotNull("The \"updated\" date was not updated on insert()", one.updated);
}

@Test
public void preUpdateShouldBeCalledOnUpdate() {
LifeCycleEntity one = new LifeCycleEntity();
one.insert();
one.update();
Assert.assertNotNull("The \"created\" date was not updated on update()", one.created);
Assert.assertNotNull("The \"updated\" date was not updated on update()", one.updated);
}

@Before
public void setUp() throws Exception {
helper.setUp();
}

@After
public void tearDown() throws Exception {
helper.tearDown();
}

@Before
public void enableLifeCycleSupport() {
PersistenceManager pm = new GaePersistenceManager();
pm = new PersistenceManagerLifeCycleWrapper(pm);
pm.init(null);
PersistenceManagerFactory.install(pm, LifeCycleEntity.class);
}
private final LocalServiceTestHelper helper = new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig(), new LocalTaskQueueTestConfig());
}

```