Thursday, October 15, 2009

Updating A Bidrectional Owned One-To-Many With A New Child

(This was originally posted to the Google App Java Google Group on September 28, 2009)  

All the way back in our first post one we demonstrated how to create both a parent and a child of a bidirectional, owned, one-to-many relationship at the same time.  This week we're going to see how to add a child to an existing parent.  We'll use the same model objects we used before:

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

    private String title;


    @OneToMany(mappedBy = "book", cascade = CascadeType.ALL)

    private List<Chapter> chapters = new ArrayList<Chapter>();

    // getters and setters
}

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

    private String title;
    private int numPages;

    @ManyToOne(fetch = FetchType.LAZY)
    private Book book;

    // getters and setters
}

Now let's assume we've already created a book with a few chapters in the datastore and we want to add a brand new chapter to a Book with a given id (we'll assume someone else is creating and closing an EntityManager named 'em' for us):

public void addChapterToBook(EntityManager em, Key bookKey, Chapter chapter) {
    em.getTransaction().begin();
    try {
        Book b = em.find(Book.class, bookKey);
        if (b == null) {
            throw new RuntimeException("Book " + bookKey + " not found!");
        }
        b.getChapters().add(chapter);

        em.getTransaction().commit();
    } finally {
        if (em.getTransaction().isActive()) {
            em.getTransaction().rollback();
        }
    }
}


JDO:

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")
public class Book {
 
    @PrimaryKey

    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key id;

    private String title;


    @Persistent(mappedBy = "book")
    @Element(dependent = "true")
    @Order(extensions = @Extension(vendorName="datanucleus", key="list-ordering", value="id asc"))
    private List<Chapter> chapters = new ArrayList<Chapter>();


    // getters and setters
}

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")

public class Chapter {
    @PrimaryKey

    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key id;

    private String title;
    private int numPages;

    @Persistent
    private Book book;

    // getters and setters

}

Now let's assume we've already created a book with a few chapters in the datastore and we want to add a brand new chapter to a Book with a given id (we'll assume someone else is creating and closing a PersistenceManager named 'pm' for us):


public void addChapterToBook(PersistenceManager pm, Key bookKey, Chapter chapter) {
    pm.currentTransaction().begin();
    try {
        // throws a runtime exception if book is not found       
        Book b = pm.getObjectById(Book.class, bookKey);
        b.getChapters().add(chapter);

        pm.currentTransaction().commit();
    } finally {
        if (pm.currentTransaction().isActive()) {
            pm.currentTransaction().rollback();
        }
    }
}
--------------------------------

The interesting thing about both of these examples is that we're not making any explicit calls to save the new Chapter.  We look up the Book identified by the Key that was passed into the function and then we manipulate the persistent state of the object by manipulating the POJO that was returned by em.fetch/pm.getObjectById.  JPA and JDO both have mechanisms that allow them to monitor the objects that you've looked up for changes.  Ever wonder what exactly the enhancer is doing to your classes?  It's adding hooks so that the persistence framework gets notified when things change (among other things).  This allows JPA and JDO to automatically flush your changes to the datastore when you commit your transaction.  If you wanted to modify the title of the Book or the number of pages in an existing Chapter the approach would be exactly the same: Start a transaction, look up the Book, make your changes, commit your transaction.  Whether you're using JPA or JDO your changes will be persisted for you without any explicit calls to change the persistent state.  This is a prime example of how JPA and JDO facilitate "transparent persistence."

12 comments:

  1. I can not succeed this jdo sample.
    during persisting it gives an exception if chapters size is zero.

    ReplyDelete
  2. I'm happy to take a look but you'll need to provide some more information. Specifically:
    What is the exception?
    What does your update code look like?

    ReplyDelete
  3. Can you follow this url.
    http://groups.google.com/group/google-appengine-java/browse_thread/thread/2c3719e3ca2dc7c4/c915b1813a7ef1bf

    ReplyDelete
  4. Sorry Kemal I'm confused. That url doesn't seem to have anything to do with the example in this blog post. You originally said the example in this post wasn't working for you. Is that still true?

    ReplyDelete
  5. Allright, I will simplify with your code. Then I will post here. I need a little time.

    Thanks Max.

    ReplyDelete
  6. I'm looking at your issue on the groups thread now and I'll reply over there, I just want to keep the discussion on the blog tied to the actual posts.

    ReplyDelete
  7. It's my understanding that in jdo user classes are not included in the defaultfetchgroup. When I run your example i get a "cannot invoke method add() on null object" since the List chapters are not fetched when you call getChapters() on Book by default.
    However setting:
    @Persistent(defaultFetchGroup="true",loadFetchGroup="true")
    in chapters field and it works as outlined here. Did you miss this? or am I just doing it wrong?

    ReplyDelete
  8. Hmm, I can't reproduce the exception although I did discover that the type of the List of chapters on the Book object isn't showing because of bad html. If you change List chapters to List<Chapter> chapters does that help? I'll fix the example.

    ReplyDelete
  9. When you call:

    b.getChapters().add(chapter);

    dows JDO/JPA fetch all chapters from the datastore and then saves all of them again just to add a new chapter with a key that includes book as a parent?

    ReplyDelete
  10. I might be missing something very simple here but updating the details of a chapter for example the page number does not look that simple. After getting the book object, we can get the list of chapters for sure but how we would we get the chapter instance from the list which needs to be modified?

    ReplyDelete
  11. One question about performance and efficiency here... To persist a new Chapter object we need to retrieve the whole Book object... for me this looks like not the most efficient way to do especially if your book object is huge.
    Is there a way to just save the Chapter like I would do using a simple JDBC call with (insert into...)? Knowing that the Chapter as a reference on Book and so has its Id, this should be something possible but I cannot find a way to do it.

    ReplyDelete