Time 1: User A brings up the details page for Bug 838.
Time 2: User B brings up the details page for Bug 838.
Time 3: User A assigns Bug 838 to Jim.
Time 4: User B assigns Bug 838 to Sally.
Even though User A and User B updated Bug 838 at different times, User A's update was effectively stomped on by User B. Sure, the fact that Bug 838 was assigned to Jim at some point may be in the history for the Bug, we can't change the fact that User B made a decision to assign Bug 838 based on out-of-date information. In a bug tracking system this might not be such a big deal, but if you're doing something like compensation planning you'd much rather have your users receive an exception when they make an update based on out-of-date information. Fortunately this is easy to implement using JPA/JDO on top of App Engine. Let's use a Person object with a 'salary' property as an example.
JPA:
@Entity
public class Person {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private int salary;
@Version
private long version;
// ...getters and setters
}public void updateSalary(EntityManager em, Person p, int newSalary) {
em.getTransaction().begin();
try {
p.setSalary(newSalary);
p = em.merge(p);
em.getTransaction().commit();
} catch (RollbackException e) {
if (e.getCause() instanceof OptimisticLockException) {
handleVersionConflict(e.getCause(), p);
} else {
throw e;
}
} finally {
if (em.getTransaction().isActive()) {
em.getTransaction().rollback();
}
}
}
JDO:
@PersistenceCapable(identityType=IdentityType.APPLICATION)
@Version(strategy=VersionStrategy.VERSION_NUMBER)
public class Person {
@PrimaryKey
@Persistent(valueStrategy=IdGeneratorStrategy.IDENTITY)
private Long id;
private int salary;
// ...getters and setters
}
public void updateSalary(PersistenceManager pm, Person p, int newSalary) {
pm.currentTransaction().begin();
try {
p.setSalary(newSalary);
pm.makePersistent(p);
pm.currentTransaction().commit();
} catch (JDOOptimisticVerificationException e) {
handleVersionConflict(e, p);
} finally {
if (pm.currentTransaction().isActive()) {
pm.currentTransaction().rollback();
}
}
}
If you declare a Version field on your model object, JDO/JPA will compare the value of that field on the instance you are updating with the value of that field in the datastore. If the version numbers are equal the version number will be incremented and your model object will be persisted. If the version numbers are not equal the appropriate exception will be thrown. In the above examples I've caught this exception in the update code itself, but this is really just to illustrate what's going on. In practice I would most likely let this exception propagate out of the update method and handle it at a higher level, perhaps even as part of a generic exception handler in my presentation code.
Note that there is a performance cost to using this feature. The datastore does not support updates with predicates the way relational databases do ('update person where version = 3') so in order to perform the version comparison it needs to do an additional get() for the entity that corresponds to your model object. You'll have to decide for yourself whether or not the cost of the additional fetch is worthwhile.
Now, if you've looked at both the JPA and JDO examples you may have noticed that JPA requires the declaration of an explicit field annotated with @Version while JDO accepts an @Version annotation on the class without an explicit field. If you are using JDO and would like the version stored in an explicit field you can use a DataNucleus-specific extension to tell JDO which field to use. You can see an example of this here.
Using JPA and JDO's built-in versioning checks is an easy way to help users of your app make decisions based on up-to-date information. Try it out!