Sunday, May 30, 2010

Persistence as Aspect - Playing around with AspectJ and JPA

With logging, persistence is often used as an example where Aspect Oriented solves tangling problem. Instead of writing the persisting code to persist an object, one may consider persistency as an aspect and then write the persistence aspect. 

With ORM, the persistency codes have been already better. Instead of  hardcoding queries, developers work directly with object. That looks promising. But now, have a look at the entity class:

@Entity
public class Flight {
  @Column(name="ID")
  @Id  private int id; 
  private String number;
  private String airline;
  private String departureAirport;
  private String arrivalAirport;


  @Temporal(TemporalType.TIMESTAMP)
  private Date departureTime;
 

  @Temporal(TemporalType.TIMESTAMP)
  private Date arrivalTime;
...
}

As you may see, the persistent code tangles the object model. To avoid this tangling, of course, we can write the mapping and the definition in mapping file. But, I'm not really fan of XML because it is not compiled and it has a tendency to be more verbose.

Furthermore, if applications don't care about id, why should we define ID. I remember that thinking of relation to other class as a primary key is not a good Object Oriented programming practice.

So, why don't write PersistenceAspect as the way to annotate the class to make it persistable, after all, using intertype declaration we can add a member and also the annotations ? That's exactly the idea coming to my mind this week, and I made a try to implement the PersistentAspect. In this article, I use EclipseLink as the ORM.

Let's start now. But before starting, let's write the class that persisting a Flight object.
public class FlightMain {
  public static void main(String args[]) throws ParseException {
    EntityManagerFactory emf = 
      Persistence.createEntityManagerFactory("jpa");
    EntityManager em = emf.createEntityManager();
    try {
      Flight f1 = 
            createFlight(

               "AF""1204"
               "CDG""201005100800"
               "NCE""201005100910" );
      Flight f2 = 
            createFlight(

                  "AF""1274"
                  "CDG""201005101000"
                  "NCE""201005101110" );
      em.getTransaction().begin();
      em.persist(f1);
      em.persist(f2);
      em.getTransaction().commit();
    catch (Exception e) {
      e.printStackTrace();
    }
    finally {
      em.close();
      emf.close();
    }
  }


  private  static Flight createFlight(
      String airline, 
      String number,
      String dep, 
      String depTime, 
      String arr, 
      String arrTime
      throws ParseException {


    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmm");
    Flight flight = new Flight();
    flight.setAirline(airline);
    flight.setNumber(number);
    flight.setDepartureAirport(dep);
    flight.setDepartureTime(sdf.parse(depTime));
    flight.setArrivalAirport(arr);
    flight.setArrivalTime(sdf.parse(arrTime));

    return flight;
  }
}
To start with, I don't want to have Id in my Flight class. All right, move it to the aspect then.

public aspect PersistentAspect {
  @Id  private int Flight.id; 
..
}
To make the story more interesting, let's make the aspect to be responsible to generate id. To do this, we will advise Flight constructor that sets the id. Something like:


public privileged aspect PersistentAspect {

...
  @Id  private int Flight.id; 
  private int generatedId = 10000;
  pointcut flightCreation(Flight f):

      executionFlight.new()) && this(f);
  after(Flight f: flightCreation(f) {
    f.id = generatedId++;
  }
}



With this aspect definition , we can run FlightMain above. It should work... 
Unfortunately, no. I had error because a colum did not exist. Inspecting the log, I had the following query:

INSERT INTO FLIGHT (
AJC$INTERFIELD$COM_SCATLING_PERSITENCE_PERSISTENTASPECT$ID
ARRIVALTIME, 
DEPARTUREAIRPORT, 
ARRIVALAIRPORT, 
DEPARTURETIME, 
NUMBER, 
AIRLINE)

Of course, EclipseLink works with the woven class of Flight, and the woven class has that AJC$INTERFIELD...$ID instead of ID. Well, then it means that we need to explicitly define the column name for intertype declaration. That's what we're going to do:
public aspect PersistentAspect {

...
  @Column(name="ID")
  @Id  private int Flight.id; 

  private int generatedId = 10000;
  pointcut flightCreation(Flight f)

     executionFlight.new()) && this(f);


  after(Flight f: flightCreation(f) {     f.id = generatedId++;
  }
}

With this modification included now, the FlightMain works fine, two Flight objects are stored in the database.


Not satisfied with extracting id from Flight, I also want to strip @Entity from Flight class and also the temporal type for arrivalTime and departureTime. My Flight class becomes:


public class Flight {
  private String number;
  private String airline;
  private String departureAirport;
  private String arrivalAirport;
  private Date departureTime;
  private Date arrivalTime;

..
}

And my aspect becomes:
public privileged aspect PersistentAspect {

  @Column(name="ID")
  @Id  private int Flight.id; 
  private int generatedId = 10000;

  pointcut flightCreation(Flight f)

     executionFlight.new()) && this(f);

  after(Flight f: flightCreation(f) {
    f.id = generatedId++;
  }
  /** Make Flight class an Entity 
   
   */

  declare @type : Flight: @Entity;

  /** Declares the departure time as timestamp.
   
   */
  declare @field: Date Flight.departureTime: 

     @Temporal(TemporalType.TIMESTAMP);

  

  /** Declares the arrival time as timestamp.
   *  
   */ 
  declare @field: Date Flight.arrivalTime: 

     @Temporal(TemporalType.TIMESTAMP);
}
And yes, it works !



Conclusion and Last Remarks
In this article, aspect can be used as alternative to XML object mapping. At least, we can reduce the need of XML object mapping.   I foun d the need to explicitly intertyped column with ID is not convenient. Maybe, AspectJ should weave the Column annotation itself. 



No comments: