Sunday, March 21, 2010

Executing Simple Joins Across Owned Relationships

As promised, today I'm going to explain how to use simple joins with owned relationships. This requires some additional setup because the physical layout of an owned relationship is not the same as the physical layout of an unowned relationship. Specifically, with an unowned relationship, the keys of the child entities are stored in a property on the parent, whereas with an owned relationship the keys of the child entities are not stored in a property on the parent - ownership is instead modeled using the hierarchical structure of the child key.  Simple joins depend on the physical layout we get with unowned relationships, so if we're going to use simple joins with owned relationships we're going to need to change the way we store them.

Fortunately, as of SDK 1.3.1, there is a configuration option that changes the physical layout of owned relationships to match the physical layout of unowned relationships while preserving the hierarchical structure of the child key.  Nice!  You can enable this option by adding the following to your config:

<property name="datanucleus.appengine.storageVersion" value="WRITE_OWNED_CHILD_KEYS_TO_PARENTS"/>

If you're using JPA, add this to persistence.xml. If you're using JDO, add this to jdoconfig.xml. If you don't have any existing data in production you're ready to move on to the examples. If you do have existing data there's more work to do: data migration. If you're like me then you get sad, then angry, then hungry, then sad again whenever you see those words so I apologize for just throwing them in your face like that, but if you have existing data there's really no way around it.  See, setting this config option will cause all newly created and updated parents to be written with a property containing child keys, but all your existing data is still sitting there in the old layout. If you want the results of your simple joins to be accurate (and you do want this, right?) you're going to need to migrate all your existing data. I'll explain how to do this in a later post (note that I said 'a later post' not 'the next post' - I may not get to this for awhile).

Let's go back to our college example.  We have Students, Credits, and Honors Thesis.  Every Student has zero or more Credits and zero or one Honors Thesis.

JPA:
@Entity
public class Student {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String year;

  @OneToMany(cascade = CascadeType.ALL)
  private List<Credit> credits;

  @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
  private HonorsThesis thesis;
}

@Entity
public class Credit {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Key key;

  private String department;
}

@Entity
public class HonorsThesis {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Key key;

  private boolean complete;
}


JDO:
@PersistenceCapable(detachable = "true")
public class Student {
  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  private Long id;

  private String year;

  @Element(dependent = "true")
  private List<Credit> credits;

  @Persistent
  private HonorsThesis thesis;
}

@PersistenceCapable(detachable = "true")
public class Credit {
  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  private Key key;

  private String department;
}

@PersistenceCapable(detachable = "true")
public class HonorsThesis {
  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  private Key key;

  private boolean complete;
}
---------------------------------------
Here's a query that returns all Seniors with at least one credit from the Philosophy department:
JPA:
Query q = em.createQuery("select from " + Student.class.getName() 
  + " s JOIN s.credits c where "
  + "c.department = 'Philosophy' and "
  + "s.year = 'Senior'");

JDO:
Query q = pm.newQuery("select from " + Student.class.getName() 
  + " where "
  + "credits.contains(c) && c.department == 'Philosophy' && "
  + "year == 'Senior'");
q.declareVariables(Credit.class.getName() + " c");

---------------------------------------

And here's a query that returns all Seniors who have completed an Honors Thesis:
JPA:
Query q = em.createQuery("select from " + Student.class.getName() 
  + " s JOIN s.thesis t where "
  + "t.complete = true and "
  + "s.year = 'Senior'");

JDO:
Query q = pm.newQuery("select from " + Student.class.getName() + " where "
  + "thesis == t && t.complete == true && "
  + "year == 'Senior'");
q.declareVariables(HonorsThesis.class.getName() + " t");


---------------------------------------
Unlike the unowned relationship examples we covered in the last post, we're now firmly back in the land of standard JPA and JDO.  Good!  That's where we want to be.  Give it a try and let me know what you think!  In the next post, hopefully less than 6 weeks away this time, I'll explain how simple joins work under the hood.