Brian's Waste of Time

Thu, 08 Jul 2004

OJB 1.1 Fun -- PersistenceAware

With OJB 1.0 finally released there has been tons of activity on cvs HEAD (OJB 1.1) for OJB. Most of my work thus far has been to beef up the capabilities of the most functional OJB client API -- the PersistenceBroker. Added change detection (via listeners) which works pretty nicely: it will do a copy-fields-on-read and compare-on-commit for classes which are not aware of persistence. This is basically the OTM and ODMG behavior, Hibernate behavior, TopLink behavior etc. It requires some small amount of overhead (basically 2x memory usage, and a fieldwise compare on the whole object graph on commit). The more fun one is that when classes are willing to maintain their own persistent state this overhead disappears =)

The contract for maintaining your own state is pretty simple: you'll be given a reporter to which you can report state changes (field gets dirtied, etc), and need to be able to restore your values on rollback when signalled. That's it. Here is a Beer which knows how to maintain its state.

import org.apache.ojb.broker.dirty.PersistenceAware;
import org.apache.ojb.broker.dirty.StateReporter;

public class SmartBeer extends DomesticBeer implements PersistenceAware {
    private StateReporter reporter = null;
    private Integer iq = new Integer(100);

    private Double oldPrice = null;
    private String oldBrand = null;
    private Integer oldIQ = null;

    public void setStateReporter(StateReporter reporter) {
        this.reporter = reporter;
    }

    public void restore() {
        if (oldIQ != null) this.iq = oldIQ;
        if (oldBrand != null) this.setBrand(oldBrand);
        if (oldPrice != null) this.setPrice(oldPrice.doubleValue());
        this.oldBrand = null;
        this.oldPrice = null;
        this.oldIQ = null;
    }

    public int getIQ() {
        return iq.intValue();
    }

    public void setIQ(int new_iq) {
        if (reporter != null && reporter.isTransactional()) {
            this.oldIQ = iq;
            reporter.makeDirty("iq");
        }
        this.iq = new Integer(new_iq);
    }

    public void setPrice(double price) {
        if (reporter != null && reporter.isTransactional()) {
            this.oldPrice = new Double(this.getPrice());
            reporter.makeDirty("price");
        }
        super.setPrice(price);
    }

    public void setBrand(String brand) {
        if (reporter != null && reporter.isTransactional()) {
            this.oldBrand = this.getBrand();
            reporter.makeDirty("brand");
        }
        super.setBrand(brand);
    }
}

Pretty gnarly, if straightforward. This smells of... crosscutting concern! Here is a less intelligent Beer

public class LessIntelligentBeer implements Beer {
    
    private String brand;
    private Double price;
    private Integer id;

    public String getBrand() {
        return brand;
    }

    public void setBrand(String b) {
        this.brand = b;
    }

    public double getPrice() {
        return price.doubleValue();
    }

    public void setPrice(double price) {
        this.price = new Double(price);
    }
}

This beer would need to use the less efficient option, except that we can go ahead and enhance it via a static aspect and helper class:

package org.skife.ojb;

import org.apache.ojb.broker.dirty.StateReporter;
import org.aspectj.lang.Signature;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public aspect PersistenceEnhancer {
    pointcut fieldSet(PersistenceEnhanced aware): set(* PersistenceEnhanced+.*) && target(aware);

    before(PersistenceEnhanced aware) :  fieldSet(aware) {
        try {
            Signature signature = thisJoinPoint.getSignature();
            Field field = signature.getDeclaringType().getDeclaredField(signature.getName());
            if (!Modifier.isTransient(field.getModifiers())) {
                field.setAccessible(true);
                StateReporter rep = aware.getStateReporter();
                if (rep != null && rep.isTransactional())
                {
                    rep.makeDirty(field.getName());
                    try {
                        aware.makeDirty(field, field.get(aware));
                    }
                    catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }
}

package org.skife.ojb;

import org.apache.ojb.broker.dirty.PersistenceAware;
import org.apache.ojb.broker.dirty.StateReporter;

import java.util.HashMap;
import java.util.Iterator;
import java.lang.reflect.Field;

public class PersistenceEnhanced implements PersistenceAware {
    private transient StateReporter reporter;
    private final HashMap oldValues = new HashMap();

    public void setStateReporter(StateReporter stateReporter) {
        this.reporter = stateReporter;
        oldValues.clear();
    }

    public StateReporter getStateReporter() {
        return reporter;
    }

    public void makeDirty(Field field, Object oldValue) {
        oldValues.put(field, oldValue);
    }

    public void restore() {
        for (Iterator itty = oldValues.keySet().iterator(); itty.hasNext();) {
            Field field = (Field) itty.next();
            try {
                field.setAccessible(true);
                field.set(this, oldValues.get(field));
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
        oldValues.clear();
    }
}

This aspect simply defines behavior on PeristenceEnhanced derived classes to capture field changes, report dirties, and a facility for restoring dirties. Now we need one more aspect which gives our specific cases:

import org.skife.ojb.PersistenceEnhanced;

public aspect BeerEnhancer
{
    declare parents: LessIntelligentBeer extends PersistenceEnhanced;
}

And LessIntelligentBeer now has the same performance characteristics as carefully managed updates in the PB api, but the same state management behavior as the higher overhead ODMG or OTM api's, with no extra code (assuming you use these aspects). This only works against OJB's cvs HEAD, and the api's will likely change before release, but the idea is nice. The other key thing is that the behavior is the same against PersistenceAware classes and ones which are not -- you just get better performance (speed and memory) with copy-on-write and no need for comparisons on commit!

Special thanks to Jon Tirsen for sample code for finding Field instances from pointcuts.

1 writebacks [/src/java/ojb] permanent link