views:

722

answers:

3

I'm having trouble figuring out the proper way to update "nested" data using Google App Engine and JDO. I have a RecipeJDO and an IngredientJDO.

I want to be able to completely replace the ingredients in a given recipe instance with a new list of ingredients. Then, when that recipe is (re)persisted, any previously attached ingredients will be deleted totally from the datastore, and the new ones will be persisted and associated with the recipe.

Something like:

  // retrieve from GAE datastore
  RecipeJDO recipe = getRecipeById();    

  // fetch new ingredients from the user
  List<IngredientJDO> newIngredients = getNewIngredients();
  recipe.setIngredients(newIngredients);

  // update the recipe w/ new ingredients
  saveUpdatedRecipe(recipe);

This works fine when I update (detached) recipe objects directly, as returned from the datastore. However if I copy a RecipeJDO, then make the aforementioned updates, it ends up appending the new ingredients, which are then returned along with the old ingredients when the recipe is then re-fetched from the datastore. (Why bother with the copy at all? I'm using GWT on the front end, so I'm copying the JDO objects to DTOs, the user edits them on the front end, and then they are sent to the backend for updating the datastore.)

Why do I get different results with objects that I create by hand (setting all the fields, including the id) vs operating on instances returned by the PersistenceManager? Obviously JDO's bytecode enhancement is involved somehow.

Am I better off just explicitly deleting the old ingredients before persisting the updated recipe?

(Side question- does anyone else get frustrated with ORM and wish we could go back to plain old RDBMS? :-)

+2  A: 

Short answer. Change RecipeJDO.setIngredients() to this:

public void setIngredients(List<IngredientJDO> ingredients) {
  this.ingredients.clear();
  this.ingredients.addAll(ingredients);
}

When you fetch the RecipeJDO, the ingredients list is not a real ArrayList, it is a dynamic proxy that handles the persistence of the contained elements. You shouldn't replace it.

While the persistence manager is open, you can iterate through the ingredients list, add items or remove items, and the changes will be persisted when the persistence manager is closed (or the transaction is committed, if you are in a transaction). Here's how you would do the update without a transaction:

public void updateRecipe(String id, List<IngredientDTO> newIngredients) {
  List<IngredientJDO> ingredients = convertIngredientDtosToJdos(newIngredients);
  PersistenceManager pm = PMF.get().getPersistenceManager();
  try {
    RecipeJDO recipe = pm.getObjectById(RecipeJDO.class, id);
    recipe.setIngredients(ingredients);
  } finally {
    pm.close();
  }
}

If you never modify the IngredientJDO objects (only replace them and read them), you might want to make them Serializable objects instead of JDO objects. If you do that, you may be able to reuse the Ingredient class in your GWT RPC code.

Incidentally, even if Recipe was not a JDO object, you would want to make a copy in the setIngredients() method, otherwise someone could do this:

List<IngredientJDO> ingredients = new ArrayList<IngredientJDO>;
// add items to ingredients
recipe.setIngredients(ingredients);
ingredients.clear(); // Woops! Modifies Recipe!
NamshubWriter
Sorry, this does not solve the problem. I didn't articulate it very well. When I get the (non-JDO) recipes back from GWT, I create new RecipeJDO objects (I literally call "new RecipeJDO()") , copy all the fields in, and then do pm.makePersistent() on it. It's the creation via "new" that is causing the problem. If I instead fetch the RecipeJDO from the datastore, and then copy the fields in, all is well. I suppose the solution is to fetch the old RecipeJDO from the datastore, copy the fields into THAT OBJECT, and then update THAT OBJECT. SQL is looking better all the time. ;-)
Caffeine Coma
If you are updating an existing recipe, don't create a new RecipeJDO object. Creating a new ReceipeJDO object and calling makePersistent() is like doing an SQL INSERT. The data was already inserted when the recipe was first created. After that, you should update it.
NamshubWriter
Yes, but I'm setting the id of the RecipeJDO to the id of an existing recipe in the datastore, so it should work as an update. And it does, except that the (new) ingredients are added to the list of existing ones, rather than replacing them.It seems like maybe I need to retrieve the RecipeJDO from the PersistenceManager, update its fields, remove any existing ingredients (using its original list), and then add the new ingredients, then re-persist it. Although this will likely do what I need, it seems like an awful lot of work that kind of negates the utility of an ORM in the first place.
Caffeine Coma
The only additional work is getting the ReceipeJDO by ID from the PersistenceManager instead of creating it by the constructor. You would have to set the fields on the RecipeJDO from the GWT RPC DTO either way. Removing the existing ingredients in setIngredients() is something you want to do anyway, as I said in my answer. The need for code copying around between representations of recipe is more a function of GWT than JDO (if you used Stripes and JSP, you could avoid the GWT RPC and GWT model representations of Recipe and Ingredient)
NamshubWriter
A: 

I am facing the same problem! I would like to update an existing entity by calling makePersistent() and assigning an existent id/key! the update works fine except for nested objects! The nested objects are appended to the old ones instead of being replaced? I don't know if this is the intended behaviour or if this is a bug? I expect overwriting to have the same effect as inserting a new entity!

How about first deleting the old entity and persisting the new one in the same transaction? Does this work? I tried this but it resulted in deleting the entity completely?! I don't know why (even though I tried flushing directly after deleting)!

Hani Nabil Boustani
A: 

@NamshubWriter, not sure if you'll catch this post... regarding your comment,

(if you used Stripes and JSP, you could avoid the GWT RPC and GWT model representations of Recipe and Ingredient)

I am using Stripes and JSP, but I face the same problem. When the user submits the form back, Stripes instantiates my entity objects from scratch, and so JDO is completely ignorant of them. When I call PersistenceManager.makePersistent on the root object, the previous version is correctly overwritten - with one exception, its child objects are appended to the List<child> of the previous version.

If you could suggest any solution (better than manually copying the object fields) I would greatly appreciate.

(seeing as Stripes is so pluggable, I wonder if I can override how it instantiates the entity objects...)

Iain