EJB 3.0 Persistence – ManyToMany relations or the objectification of the intersection table

12

I have done a fair bit of Entity Relationship Modeling. It’s one of the things I probably like most about my job. And one of the things in ERD I have been brought up in was: you can start out by modeling Many-to-Many relationships, but in the second stage of your model – when you prepare it for transformation to your technical database design – it is best to ‘resolve’ those nasty many-to-many relationships. Because the database cannot handle them. We cannot have a collection of references stored in a single column or something like that. And quite often there was a good reason for replacing the many-to-many relationship by a so called "intersection entity" as we had to cater for or at least could think of some meaningful characteristics for the relationship itself. How long it lasted or what special type of relationship it was. Or how much it cost. And before the transformation to database design started, we had rid ourselves of the many-to-many’s.

Funnily enough, EJB 3.0 Persistence is again about Entities. Java Classes that are also called POJOs (Plain Old Java Objects) or Domain Classes. An Entity is nothing but a Java Bean – a POJO with some setters and getters – about which we say to the EJB 3.0 EntityManager: it can be persisted. It maps to a table. If we create one, you can Insert into a table. If we look for one, you can Select it from that table. And EJB 3.0 Persistence allows us to specify relations, not just the ones our database can handle, but also the Many-To-Many that it choked on. In this article we will take a brief look at the Many-To-Many relation in EJB 3.0. We will do this using the GlassFish Reference Implementation of EJB 3.0 Persistence and we work strictly outside the container in a stand-alone J2SE application. For more on getting GlassFish installed and starting out with EJB 3.0 Persistence, also see my previous posts Getting Started with EJB 3.0 Persistence out-of-container using the Reference Implementation (GlassFish) and Diving deeper into EJB 3.0 Persistence with GlassFish (RI) – still out of container.

....

Here you see an example of an ERD, with a Many-To-Many relation. Each Book can have multiple Authors – including people who design the cover, write the foreword and make a guest appearance by write a single chapter. Authors typically will have more than one book they have written. We could argue about the optionality of this relation: can someone be called an Author wihtout having written any books? And can there be a book with now Author? Let’s settle for this definition for now.

The next stage in convention ERD, geared towards database design, would have this many-to-many resolved, usually resulting in an intersection-entity called something like BOOKS_AUTHORS or in this case: Authorship:

 

We have found an attribute to, to give some life to the Intersection Entity: Contributor Type which indicates in what capacity the Author has contributed to the Book. We could have add his fee, the time it took to make this contribution etc.

Now however we have an ERD that we can easily turn into a database design and subsequently tables, columns and foreign keys in a relational database. Here is what the database looks like:

 

Mapping Java Entities to (Intersection) Tables

When we want to leverage this Database in our Java Application, we have to create the Entities or Domain Objects, typically classes Author and Book. And we have to map those Entities to the underlying database tables. Here is how we do that using EJB 3.0 Persistence.

package nl.amis.als.ejb30;<br /><br />import java.io.Serializable;<br /><br />import java.util.Collection;<br /><br />import javax.persistence.CascadeType;<br />import javax.persistence.Column;<br />import javax.persistence.Entity;<br />import javax.persistence.Id;<br />import javax.persistence.Table;<br />import javax.persistence.ManyToMany;<br />import javax.persistence.JoinTable;<br />import javax.persistence.JoinColumn;<br />import javax.persistence.CascadeType;<br /><br /><br />@Entity<br />@Table(name=&quot;ALS_BOOKS&quot;)<br />public class Book implements Serializable {<br /><br /><br />    private Long id;<br /><br />    private String isbn;<br /><br />    private Long publishMonth;<br /><br />    private Long publishYear;<br /><br />    private String title;<br /><br />    private Collection&lt;Author&gt; authors;<br /><br />    public Book() {<br />    }<br /><br />    public Book(Long id, String title) {<br />        this.id = id;<br />        this.title = title;<br />    }<br /><br />    @ManyToMany(cascade=CascadeType.ALL)<br />    @JoinTable(table = @Table(name = &quot;als_authorships&quot;), joinColumns = {<br />           @JoinColumn(name = &quot;bok_id&quot;)<br />       }, inverseJoinColumns = {<br />           @JoinColumn(name = &quot;atr_id&quot;)<br />       })<br />    public Collection&lt;Author&gt; getAuthors() {<br />        return this.authors;<br />    }<br />    <br />    public void setAuthors(Collection&lt;Author&gt; authors) {<br />        this.authors = authors;<br />    }<br />    <br />    @Id<br />    @Column(name=&quot;ID&quot;, nullable=false)<br />    public Long getId() {<br />        return id;<br />    }<br /><br />    public void setId(Long id) {<br />        this.id = id;<br />    }<br /><br />    @Column(name=&quot;ISBN&quot;)<br />    public String getIsbn() {<br />        return isbn;<br />    }<br /><br />    public void setIsbn(String isbn) {<br />        this.isbn = isbn;<br />    }<br /><br />    @Column(name=&quot;PUBLISH_MONTH&quot;)<br />    public Long getPublishMonth() {<br />        return publishMonth;<br />    }<br /><br />    public void setPublishMonth(Long publishMonth) {<br />        this.publishMonth = publishMonth;<br />    }<br /><br />    @Column(name=&quot;PUBLISH_YEAR&quot;)<br />    public Long getPublishYear() {<br />        return publishYear;<br />    }<br /><br />    public void setPublishYear(Long publishYear) {<br />        this.publishYear = publishYear;<br />    }<br /><br />    @Column(name=&quot;TITLE&quot;, nullable=false)<br />    public String getTitle() {<br />        return title;<br />    }<br /><br />    public void setTitle(String title) {<br />        this.title = title;<br />    }<br /><br />}<br />&nbsp;

And its companion, the Author Class:

package nl.amis.als.ejb30;<br /><br />import java.io.Serializable;<br /><br />import java.util.Collection;<br /><br />import javax.persistence.CascadeType;<br />import javax.persistence.Column;<br />import javax.persistence.Entity;<br />import javax.persistence.Id;<br />import javax.persistence.Table;<br />import javax.persistence.ManyToMany;<br />import javax.persistence.JoinTable;<br />import javax.persistence.JoinColumn;<br />import javax.persistence.CascadeType;<br /><br /><br />@Entity<br />@Table(name=&quot;ALS_AUTHORS&quot;)<br />public class Author implements Serializable {<br />    private String biography;<br /><br />    private String firstName;<br /><br />    private Long id;<br /><br />    private String initials;<br /><br />    private String lastName;<br />    <br />    private Collection&lt;Book&gt; books;<br /><br />    public Author() {<br />    }<br /><br />    public Author
(Long id) {<br />        this.id = id
;<br />    }<br /><br />    @Column(name=&quot;BIOGRAPHY&quot;)<br />    public String getBiography() {<br />        return biography;<br />    }<br /><br />    public void setBiography(String biography) {<br />        this.biography = biography;<br />    }<br /><br />    @Column(name=&quot;FIRST_NAME&quot;)<br />    public String getFirstName() {<br />        return firstName;<br />    }<br /><br />    public void setFirstName(String firstName) {<br />        this.firstName = firstName;<br />    }<br /><br />    @Id<br />    @Column(name=&quot;ID&quot;, nullable=false)<br />    public Long getId() {<br />        return id;<br />    }<br /><br />    public void setId(Long id) {<br />        this.id = id;<br />    }<br /><br />    @Column(name=&quot;INITIALS&quot;)<br />    public String getInitials() {<br />        return initials;<br />    }<br /><br />    public void setInitials(String initials) {<br />        this.initials = initials;<br />    }<br /><br />    @Column(name=&quot;LAST_NAME&quot;)<br />    public String getLastName() {<br />        return lastName;<br />    }<br /><br />    public void setLastName(String lastName) {<br />        this.lastName = lastName;<br />    }<br />    <br />    @ManyToMany(cascade=CascadeType.ALL)<br />    @JoinTable(table = @Table(name = &quot;als_authorships&quot;), joinColumns = {<br />           @JoinColumn(name = &quot;atr_id&quot;)<br />       }, inverseJoinColumns = {<br />           @JoinColumn(name = &quot;bok_id&quot;)<br />       })<br />    public Collection&lt;Book&gt; getBooks() {<br />        return this.books;<br />    }<br />    public void setBooks(Collection&lt;Book&gt; books) {<br />        this.books = books;<br />    }<br />        <br />}<br /><br />

Note: in both Entities, we have left out a number of optional properties, for brevity and clarity.

The LibraryService class that provides services for these entities – grantedly a pathetic set of services. In fact: service.

package nl.amis.als.ejb30;<br /><br />import java.util.Collection;<br />import java.util.List;<br /><br />import javax.persistence.EntityManager;<br />import javax.persistence.EntityManagerFactory;<br />import javax.persistence.Persistence;<br /><br />public class LibraryService {<br />    public LibraryService() {<br /><br />        EntityManagerFactory emf =<br />            Persistence.createEntityManagerFactory(&quot;pu2&quot;);<br />        // create EntityManager<br />        EntityManager em = emf.createEntityManager();<br />        this.setEntityManager(em);<br />    }<br /><br />    private EntityManager _entityManager;<br /><br />    public EntityManager getEntityManager() {<br />        return _entityManager;<br />    }<br /><br />    public void setEntityManager(EntityManager entityManager) {<br />        _entityManager = entityManager;<br />    }<br /><br />    public List&lt;Book&gt; findAllBook()  {<br />        return getEntityManager().createQuery(&quot;select object(o) from Book o&quot;).getResultList();<br />    }<br /><br />    public static void main(String[] args) {<br />       LibraryService libraryService = new LibraryService();<br />       Collection&lt;Book&gt; books = libraryService.findAllBook();<br />       for (Book b : books) { <br />         System.out.println(b.getTitle());<br />         for (Author a: b.getAuthors()) {<br />             System.out.println(&quot; - &quot;+a.getFirstName()+&quot; &quot;+a.getLastName());<br />         }<br />       }        <br />    }<br /><br />}<br /><br />

You can that the LibraryService class references a Persistency Unit called pu2. This refers to a definition in the persistence.xml file, in the classes/META-INF directory. This files looks like this:

&lt;persistence xmlns=&quot;http://java.sun.com/xml/ns/persistence&quot;&gt;<br />    &lt;persistence-unit name=&quot;pu2&quot;&gt;<br />        &lt;!-- Provider class name is required in Java SE --&gt;<br />        &lt;provider&gt;oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider&lt;/provider&gt;<br />        &lt;!-- All persistence classes must be listed --&gt;<br />        &lt;class&gt;nl.amis.als.ejb30.Author&lt;/class&gt;<br />        &lt;class&gt;nl.amis.als.ejb30.Book&lt;/class&gt;<br />        &lt;properties&gt;<br />            &lt;!-- Provider-specific connection properties --&gt;<br />            &lt;property name=&quot;jdbc.driver&quot; value=&quot;oracle.jdbc.driver.OracleDriver&quot;/&gt;<br />            &lt;property name=&quot;jdbc.connection.string&quot; value=&quot;jdbc:oracle:thin:@localhost:1521:ORCL&quot;/&gt;<br />            &lt;property name=&quot;jdbc.user&quot; value=&quot;als&quot;/&gt;<br />            &lt;property name=&quot;jdbc.password&quot; value=&quot;als&quot;/&gt;<br />            &lt;!-- Provider-specific settings --&gt;<br />            &lt;property name=&quot;toplink.logging.level&quot; value=&quot;NORMAL&quot;/&gt;<br />        &lt;/properties&gt;<br />    &lt;/persistence-unit&gt;<br />&lt;/persistence&gt; &nbsp;<br />

The output of running the LibraryService class is something like 

The Data Warehouse Toolkit
 - Ralph Kimball
 - Ted Husted
Java Tools for eXtreme Programming
 - Richard Hightower
 - Nicholas Lesiecki
J2EE Design and Development
 - Rod Johnson
CSS, Pocket Reference
 - Eric Meyer
JUnit in Action
 - Vincent Massol
 - Ted Husted
Building Oracle XML Applications
 - Steve Muench
Core Java 2, Volume I: Fundamentals
 - Gary Cornell
 - Cay Horstmann
XSLT Cookbook, Solutions and Examples for XML and XSLT Developers
 - Sal Mangano
Core J2EE Patterns: Best Practices and Design Strategies
 - Dan Malks
 - John Crupias
 - Deepak Alur

If we increase the logging – by changing the toplink.logging.level from NORMAL to FINE in the persistence.xml file -, we get a little more insight in what is going on internally:

[TopLink Info]: 2006.01.02 08:13:17.858–ServerSession(27978063)–Thread(Thread[main,5,main])–file:/C:/glassfish/ejb30_se/se1/classes-pu2 login successful
[TopLink Fine]: 2006.01.02 08:13:18.088–ServerSession(27978063)–Connection(5863106)–Thread(Thread[main,5,main])–SELECT ID, PUBLISH_MONTH, PUBLISH_YEAR, ISBN, TITLE FROM ALS_BOOKS

J2EE Design and Development
[TopLink Fine]: 2006.01.02 08:13:18.288–ServerSession(27978063)–Connection(5863106)–Thread(Thread[main,5,main])–SELECT t1.ID, t1.BIOGRAPHY, t1.FIRST_NAME, t1.INITIALS, t1.LAST_NAME FROM als_authorships t0, ALS_AUTHORS t1 WHERE ((t0.bok_id = 5) AND (t1.ID = t0.atr_id))
 - Rod Johnson
CSS, Pocket Reference
[TopLink Fine]: 2006.01.02 08:13:18.298–ServerSession(27978063)–Connection(14900151)–Thread(Thread[main,5,main])–SELECT t1.ID, t1.BIOGRAPHY, t1.FIRST_NAME, t1.INITIALS, t1.LAST_NAME FROM als_authorships t0, ALS_AUTHORS t1 WHERE ((t0.bok_id = 6) AND (t1.ID = t0.atr_id))
 - Eric Meyer
JUnit in Action
[TopLink Fine]: 2006.01.02 08:13:18.298–ServerSession(27978063)–Connection(5863106)–Thread(Thread[main,5,main])–SELECT t1.ID, t1.BIOGRAPHY, t1.FIRST_NAME, t1.INITIALS, t1.LAST_NAME FROM als_authorships t0, ALS_AUTHORS t1 WHERE ((t0.bok_id = 23) AND (t1.ID = t0.atr_id))
 - Vincent Massol
 - Ted Husted
Building Oracle XML Applications
[TopLink Fine]: 2006.01.02 08:13:18.308–ServerSession(27978063)–Connection(14900151)–Thread(Thread[main,5,main])–SELECT t1.ID, t1.BIOGRAPHY, t1.FIRST_NAME, t1.INITIALS, t1.LAST_NAME FROM als_authorships t0, ALS_AUTHORS t1 WHERE ((t0.bok_id = 24) AND (t1.ID = t0.atr_id))
 - Steve Muench
Core Java 2, Volume I: Fundamentals
[TopLink Fine]: 2006.01.02 08:13:18.308–ServerSession(27978063)–Connection(5863106)–Thread(Thread[main,5,main])–SELECT t1.ID, t1.BIOGRAPHY, t1.FIRST_NAME, t1.INITIALS, t1.LAST_NAME FROM als_authorships t0,
ALS_AUTHORS t1 WHERE ((t0.bok_id = 20) AND (t1.ID = t0.atr_id))
 - Gary Cornell
 - Cay Horstmann
XSLT Cookbook, Solutions and Examples for XML and XSLT Developers
[TopLink Fine]: 2006.01.02 08:13:18.318–ServerSession(27978063)–Connection(14900151)–Thread(Thread[main,5,main])–SELECT t1.ID, t1.BIOGRAPHY, t1.FIRST_NAME, t1.INITIALS, t1.LAST_NAME FROM als_authorships t0, ALS_AUTHORS t1 WHERE ((t0.bok_id = 25) AND (t1.ID = t0.atr_id))
 - Sal Mangano
Core J2EE Patterns: Best Practices and Design Strategies
[TopLink Fine]: 2006.01.02 08:13:18.328–ServerSession(27978063)–Connection(5863106)–Thread(Thread[main,5,main])–SELECT t1.ID, t1.BIOGRAPHY, t1.FIRST_NAME, t1.INITIALS, t1.LAST_NAME FROM als_authorships t0, ALS_AUTHORS t1 WHERE ((t0.bok_id = 21) AND (t1.ID = t0.atr_id))
 - Dan Malks
 - John Crupias
 - Deepak Alur
 

We see that GlassFish has correctly interpreted our ManyToMany annotation:

    @ManyToMany(cascade=CascadeType.ALL)<br />    @JoinTable(table = @Table(name = &quot;als_authorships&quot;), joinColumns = {<br />           @JoinColumn(name = &quot;bok_id&quot;)<br />       }, inverseJoinColumns = {<br />           @JoinColumn(name = &quot;atr_id&quot;)<br />       })<br />    public Collection&lt;Author&gt; getAuthors() {<br />        return this.authors;<br />    }<br />&nbsp;

When we ask for the Authors attribute, we are in fact asking for a Collection of Author-entity-instances. These can be retrieved from the ALS_AUTHORS table that the Author entity is mapped to by following the links defined in the JoinTable ALS_AUTHORSHIPS; column BOK_ID in this table points back at our Book’s ID attribute while column ATR_ID refers to the ID attribute of the Author. This results in query we see being executed for getting the authors for each book: join ALS_AUTHORS with ALS_AUTHORSHIPS on ATR_ID = ID and use the BOK_ID in ALS_AUTHORSHIPS to filter on authorships for the current book.

We can of course do the exact same thing from the other point of view: find all authors and for each author all books he or she has worked on. This is driven by the ManyToMany annotation in the Author entity:

    @ManyToMany(cascade=CascadeType.ALL)<br />    @JoinTable(table = @Table(name = &quot;als_authorships&quot;), joinColumns = {<br />           @JoinColumn(name = &quot;atr_id&quot;)<br />       }, inverseJoinColumns = {<br />           @JoinColumn(name = &quot;bok_id&quot;)<br />       })<br />    public Collection&lt;Book&gt; getBooks() {<br />        return this.books;<br />    }<br />&nbsp;

Code that makes use of this relation could look like this:

        Collection&lt;Author&gt; authors = libraryService.getEntityManager().createQuery(&quot;select object(o) from Author o&quot;).getResultList();;<br />        for (Author a : authors) { <br />        System.out.println(a.getFirstName()+&quot; &quot;+a.getLastName());<br />            for (Book b : a.getBooks()) { <br />              System.out.println(&quot; - &quot;+b.getTitle());<br />          }<br />        } <br />

and a possible outcome would be (note: this data is not actual Amazon data – I have been using this set for testing purposes and as such have messed around with the actual contents; as fas as I know, Ted Husted is not known for his activities in Data Warehouse country):

Richard Hightower
 - Java Tools for eXtreme Programming
 - Optimizing Oracle Performance
Nicholas Lesiecki
 - xxxJava Tools for eXtreme Programming
Ralph Kimball
 - The Data Warehouse Toolkit
 - xxxJava Tools for eXtreme Programming
Steve Muench
 - Building Oracle XML Applications
Rod Johnson
 - J2EE Design and Development
David Eisenberg
 - SVG Very Essentials
Margy Ross
Dan Malks
 - Core J2EE Patterns: Best Practices and Design Strategies
Ted Husted
 - The Data Warehouse Toolkit
 - JUnit in Action
Cary Millsap
Cay Horstmann
 - Core Java 2, Volume I: Fundamentals
Jeff Holt
Gary Cornell
 - Core Java 2, Volume I: Fundamentals
Deepak Alur
 - Core J2EE Patterns: Best Practices and Design Strategies
Budi Kurniawan
 - JavaServer Faces Programming
Sal Mangano
 - XSLT Cookbook, Solutions and Examples for XML and XSLT Developers

Now I am curious what the GlassFish EJB 3.0 EntityManager will do when I try to assign additional authors to a book. And remove them again. Let’s see:

        Collection&lt;Author&gt; authors = libraryService.getEntityManager().createQuery(&quot;select object(o) from Author o&quot;).getResultList();;<br />        Author husted = null;<br />        for (Author a : authors) { <br />           if (&quot;husted&quot;.equalsIgnoreCase(a.getLastName())) {husted = a;}<br />        }      <br />        Book jsf = libraryService.getEntityManager().find(Book.class, new Long(22));<br />        System.out.println(jsf.getTitle());<br />        jsf.getAuthors().add(husted);<br /><br />        EntityTransaction tx = libraryService.getEntityManager().getTransaction();<br />        tx.begin();<br /><br />        libraryService.getEntityManager().merge(jsf);<br />        tx.commit();<br /><br />

This results in the following logging:

[TopLink Info]: 2006.01.02 08:36:14.257–ServerSession(14707008)–Thread(Thread[main,5,main])–file:/C:/glassfish/ejb30_se/se1/classes-pu2 login successful
[TopLink Fine]: 2006.01.02 08:36:14.477–ServerSession(14707008)–Connection(10127976)–Thread(Thread[main,5,main])–SELECT ID, BIOGRAPHY, FIRST_NAME, INITIALS, LAST_NAME FROM ALS_AUTHORS
[TopLink Fine]: 2006.01.02 08:36:14.657–ServerSession(14707008)–Connection(4414010)–Thread(Thread[main,5,main])–SELECT ID, PUBLISH_MONTH, PUBLISH_YEAR, ISBN, TITLE FROM ALS_BOOKS WHERE (ID = 22)
JavaServer Faces Programming
[TopLink Fine]: 2006.01.02 08:36:14.667–ServerSession(14707008)–Connection(10127976)–Thread(Thread[main,5,main])–SELECT t1.ID, t1.BIOGRAPHY, t1.FIRST_NAME, t1.INITIALS, t1.LAST_NAME FROM als_authorships t0, ALS_AUTHORS t1 WHERE ((t0.bok_id = 22) AND (t1.ID = t0.atr_id))
[TopLink Fine]: 2006.01.02 08:36:14.698–UnitOfWork(19419092)–Connection(15453627)–Thread(Thread[main,5,main])–INSERT INTO als_authorships (atr_id, bok_id) VALUES (27, 22)

Clearly adding an Author to a Book’s Authors collection and merging (EJB 3.0 equivalent to update or persist changed entity) that Book through the EntityManager results in a proper insert. Let’s see whether removing an Author from a Collection has the same effect.

        tx.begin();<br />        jsf.getAuthors().remove(husted);<br />        libraryService.getEntityManager().merge(jsf);<br />        tx.commit();<br />

And of course it does:

[TopLink Fine]: 2006.01.02 08:41:39.895–UnitOfWork(22897006)–Connection(32380043)–Thread(Thread[main,5,main])–DELETE FROM als_authorships WHERE ((atr_id = 27) AND (bok_id = 22))
 

Of course when the intersection has properties of its own, we cannot get away with (only) the of use ManyToMany. In that case, we have to create an Entity mapped to the Intersection Table. However, we can still use ManyToMany for retrieval and even for manipulation if either the intersection entity’s attributes are optional or values are provided by the database – from triggers or through default values.

Synchronization and Isolation

One of the important things I forgot to mention thusfar is the fact that the Collections that are mapped as ManyToMany relations are isolated from one another. Neither the EntityManager nor the Collections themselves take care of synchronizing each other. That means that when you add an Author to the Book’s Authors collection, no one is adding that Book the Author’s Books collectio

n. You will have to do that yourself! The most direct way is of course adding the Book to the Author’s books collection along with the addition of the Author to the Book’s authors collection. Another way is refreshing the Author after the Book has been merged – and the EntityManager flushed.

Here is a code sample of when the change in the collection at the other end is received. Note that once again I have a misunderstanding with the EntityManager’s ignorance with regard to the identiy of objects. Retrieving the same object – Author Husted for example – in several operations results in different objects! An identity cache in the EntityManager would hopefully prevent these misunderstandings and frustrations!

        Collection&lt;Author&gt; authors = libraryService.getEntityManager().createQuery(&quot;select object(o) from Author o&quot;).getResultList();;<br />        Author husted = null;<br />        for (Author a : authors) { <br />           if (&quot;husted&quot;.equalsIgnoreCase(a.getLastName())) {husted = a;}<br />        }      <br />        Book jsf = libraryService.getEntityManager().find(Book.class, new Long(22));<br />        System.out.println(jsf.getTitle());<br />        jsf.getAuthors().add(husted);<br /><br />        System.out.println(&quot;Does Husted's books collection contain the JSF book now (after adding Husted to the JSF Book)? &quot;+(findBookInCollection(jsf, husted.getBooks())?&quot;Yes it does&quot;:&quot;No it does not&quot;));<br />        EntityTransaction tx = libraryService.getEntityManager().getTransaction();<br /><br />        tx.begin();<br />        husted = libraryService.getEntityManager().find(Author.class, new Long(27));<br />        System.out.println(&quot;Does Husted's books collection contain the JSF book now (after reading Husted from the database)? &quot;+(findBookInCollection(jsf, husted.getBooks())?&quot;Yes it does&quot;:&quot;No it does not&quot;));<br />        libraryService.getEntityManager().merge(jsf);<br />        tx.commit();<br />        System.out.println(&quot;Does Husted's books collection contain the JSF book now (after merging and committin the JSF Book)? &quot;+(findBookInCollection(jsf, husted.getBooks())?&quot;Yes it does&quot;:&quot;No it does not&quot;));<br />        tx.begin();<br />        husted = libraryService.getEntityManager().find(Author.class, new Long(27));<br />        libraryService.getEntityManager().refresh(husted);        <br />        System.out.println(&quot;Does Husted's books collection - after refreshing Husted - contain the JSF book now? &quot;+(findBookInCollection(jsf, husted.getBooks())?&quot;Yes it does&quot;:&quot;No it does not&quot;));<br />        husted = libraryService.getEntityManager().find(Author.class, new Long(27));<br />        libraryService.getEntityManager().flush();        <br />        libraryService.getEntityManager().refresh(husted);        <br />        System.out.println(&quot;Does Husted's books collection - after flushing and refreshing - contain the JSF book now? &quot;+(findBookInCollection(jsf, husted.getBooks())?&quot;Yes it does&quot;:&quot;No it does not&quot;));<br /><br /><br />        jsf.getAuthors().remove(husted);<br />        libraryService.getEntityManager().merge(jsf);<br />        tx.commit();<br /><br />

The output of this piece of code looks like this:

[TopLink Fine]: 2006.01.02 04:44:06.294--ServerSession(27978063)--Connection(2830910)--Thread(Thread[main,5,main])--SELECT ID, BIOGRAPHY, FIRST_NAME, INITIALS, LAST_NAME FROM ALS_AUTHORS<br />JavaServer Faces Programming<br />[TopLink Fine]: 2006.01.02 04:44:06.324--ServerSession(27978063)--Connection(7858936)--Thread(Thread[main,5,main])--SELECT t1.ID, t1.PUBLISH_MONTH, t1.PUBLISH_YEAR, t1.ISBN, t1.TITLE FROM als_authorships t0, ALS_BOOKS t1 WHERE ((t0.atr_id = 27) AND (t1.ID = t0.bok_id))<br />Does Husted's books collection contain the JSF book now (after adding Husted to the JSF Book)? No it does not<br />Does Husted's books collection contain the JSF book now (after reading Husted from the database)? No it does not<br />[TopLink Fine]: 2006.01.02 04:44:06.374--UnitOfWork(18930675)--Connection(17818297)--Thread(Thread[main,5,main])--INSERT INTO als_authorships (atr_id, bok_id) VALUES (27, 22)<br />Does Husted's books collection contain the JSF book now (after merging and committin the JSF Book)? No it does not<br />[TopLink Fine]: 2006.01.02 04:44:06.394--ServerSession(27978063)--Connection(2830910)--Thread(Thread[main,5,main])--SELECT ID, BIOGRAPHY, FIRST_NAME, INITIALS, LAST_NAME FROM ALS_AUTHORS WHERE (ID = 27)<br />Does Husted's books collection - after refreshing Husted - contain the JSF book now? No it does not<br />[TopLink Fine]: 2006.01.02 04:44:06.394--UnitOfWork(27109735)--Connection(15354046)--Thread(Thread[main,5,main])--SELECT ID, BIOGRAPHY, FIRST_NAME, INITIALS, LAST_NAME FROM ALS_AUTHORS WHERE (ID = 27)<br />[TopLink Fine]: 2006.01.02 04:44:06.404--UnitOfWork(27109735)--Connection(15354046)--Thread(Thread[main,5,main])--SELECT t1.ID, t1.PUBLISH_MONTH, t1.PUBLISH_YEAR, t1.ISBN, t1.TITLE FROM als_authorships t0, ALS_BOOKS t1 WHERE ((t0.atr_id = 27) AND (t1.ID = t0.bok_id))<br />Does Husted's books collection - after flushing and refreshing - contain the JSF book now? Yes it does<br />[TopLink Fine]: 2006.01.02 04:44:06.404--UnitOfWork(27109735)--Connection(15354046)--Thread(Thread[main,5,main])--SELECT t1.ID, t1.BIOGRAPHY, t1.FIRST_NAME, t1.INITIALS, t1.LAST_NAME FROM als_authorships t0, ALS_AUTHORS t1 WHERE ((t0.bok_id = 22) AND (t1.ID = t0.atr_id))<br />[TopLink Fine]: 2006.01.02 04:44:06.404--UnitOfWork(27109735)--Connection(15354046)--Thread(Thread[main,5,main])--SELECT t1.ID, t1.PUBLISH_MONTH, t1.PUBLISH_YEAR, t1.ISBN, t1.TITLE FROM als_authorships t0, ALS_BOOKS t1 WHERE ((t0.atr_id = 27) AND (t1.ID = t0.bok_id))<br />[TopLink Fine]: 2006.01.02 04:44:06.414--UnitOfWork(27109735)--Connection(15354046)--Thread(Thread[main,5,main])--SELECT t1.ID, t1.BIOGRAPHY, t1.FIRST_NAME, t1.INITIALS, t1.LAST_NAME FROM als_authorships t0, ALS_AUTHORS t1 WHERE ((t0.bok_id = 22) AND (t1.ID = t0.atr_id))<br />&nbsp;

Note: if you retrieve an Entity inside a transaction, you can still use that Entity outside the transaction, but it has become ‘detached’ when the transaction ended. Detached means it is no longer in a managed state which means that it cannot be refreshed anymore. So the following does not work:

        EntityTransaction tx = libraryService.getEntityManager().getTransaction();<br /><br />        tx.begin();<br />        husted = libraryService.getEntityManager().find(Author.class, new Long(27));<br />        System.out.println(&quot;Does Husted's books collection contain the JSF book now (after reading Husted from the database)? &quot;+(findBookInCollection(jsf, husted.getBooks())?&quot;Yes it does&quot;:&quot;No it does not&quot;));<br />        libraryService.getEntityManager().merge(jsf);<br />        tx.commit();<br />        libraryService.getEntityManager().refresh(husted);        <br />&nbsp;

The last line attempts to refresh an Entity that was acquired inside the transaction that has already been  committed. This results in an exception:

Exception in thread &quot;main&quot; javax.persistence.TransactionRequiredException: <br />Exception Description: No transaction is currently active<br />	at oracle.toplink.essentials.internal.ejb.cmp3.transaction.EntityTransactionWrapper.throwCheckTransactionFailedException(EntityTransactionWrapper.java:58)<br />	at oracle.toplink.essentials.internal.ejb.cmp3.transaction.base.EntityTransactionWrapper.checkForTransaction(EntityTransactionWrapper.java:59)<br />	at oracle.toplink.essentials.internal.ejb.cmp3.base.EntityManagerImpl.checkForTransaction(EntityManagerImpl.java:392)<br />	at oracle.toplink.essentials.internal.ejb.cmp3.base.EntityManagerImpl.refresh(EntityManagerImpl.java:223)<br />	at nl.amis.als.ejb30.LibraryService.main(LibraryService.java:77)<br /><br />

To my surprise – and this really seems a bug as it is a direct contradiction of the specification – the following code fails to:

 tx.begin();<br /> jsf.s

etTitle(jsf.getTitle()+&quot;?&quot;);<br /> libraryService.getEntityManager().merge(jsf);<br /> libraryService.getEntityManager().refresh(jsf);<br /> tx.commit();<br />&nbsp;

The exception thrown:

Exception in thread &quot;main&quot; java.lang.IllegalArgumentException: Can not refresh not managed object: nl.amis.als.ejb30.Book@18aab40.<br />	at oracle.toplink.essentials.internal.ejb.cmp3.base.EntityManagerImpl.refresh(EntityManagerImpl.java:225)<br />	at nl.amis.als.ejb30.LibraryService.main(LibraryService.java:76)<br /> <br />

Line 76 is the line where we have the EntityManager refresh jsf. That should not be a problem. Well, it seems to indicate that refresh can never be used…  

Resources

Source code for the Library Classes and Database Objects:  

Java EE 5 -Step by Step – Filippo Diotalevi – December 2005 – A real low threshold introduction into EJB 3.0 Inside the container using GlassFish

EJB 3.0 – Refreshing entity with values populated from database triggers by Raghu Kodali (14th October 2005) – The issue I have been thinking about a lot and indeed, Raghu writes that if your entity has attributes whose values are provided by the database, either through default values or via Database Triggers, you have to have the persist operation followed by a flush (to force the insert operation) and a refresh (to get the latest values back from the database). Now I am hoping I can build that behavior into an extended EntityManager implementation.

 

J2EE Persistence Productivity with Choice Doug Clarke Principal Product Manager OracleAS TopLink  (Presentation 2004)

Share.

About Author

Lucas Jellema, active in IT (and with Oracle) since 1994. Oracle ACE Director for Fusion Middleware. Consultant, trainer and instructor on diverse areas including Oracle Database (SQL & PLSQL), Service Oriented Architecture, BPM, ADF, Java in various shapes and forms and many other things. Author of the Oracle Press book: Oracle SOA Suite 11g Handbook. Frequent presenter on conferences such as JavaOne, Oracle OpenWorld, ODTUG Kaleidoscope, Devoxx and OBUG. Presenter for Oracle University Celebrity specials.

12 Comments

  1. The refresh() exception is not a contradiction to the specification. Per the specification, the entity passed to the merge() method becomes unmanaged, and the method returns a new T entity that IS managed. For the example, the following code would be successful:

    tx.begin();
    jsf.setTitle(jsf.getTitle()+”?”);
    jsf = libraryService.getEntityManager().merge(jsf);
    libraryService.getEntityManager().refresh(jsf);
    tx.commit();

    Make sure you don’t lose managed objects through the merge() operation — or many methods could throw the same exception refresh() did.

    –PE

  2. hello jim,

    i believe he is doing this because he wants to have the possibility to delete the data in the join_table starting from both entities (author and book). if one would be mapped by the “mappedBy” parameter starting the deletion from this entity would not be possible because it is not the “owning” side of the relation.

  3. Great article. I was wondering why you mapped both of your classes with the @JoinTable annotation, instead of using the mappedBy parameter.
    Like:
    In Books.java:

    @ManyToMany(cascade=CascadeType.ALL)
    @JoinTable(table = @Table(name = “als_authorships”), joinColumns = {
    @JoinColumn(name = “bok_id”)
    }, inverseJoinColumns = {
    @JoinColumn(name = “atr_id”)
    })
    public Collection getAuthors() {
    return this.authors;
    }

    And in Authors.java

    @ManyToMany(mappedBy=”authors”, cascade=CascadeType.ALL)
    public Collection getBooks() {
    return this.books;
    }

  4. Hello,

    Thanks for the article.
    Having a bit of trouble though, you fill the tables with data from your als_data.sql.
    Could you provide some code on how a fill up the book and the author, does the ‘ALS_AUTHORSHIPS’-table
    get filled in some ‘magical’ way ? I mean we do not have an Entity-class for that table.
    So how would I write the correct code for inserting the books – would like to do that from a small web-page.

    seen some examples out there, but they are full with errors.

    hope that you can provide som help.

    regards, Iner

  5. Good article :) I’m also wondering how to get the intersection table attribute Contributor Type.
    An Author has contributed to many books (stored in the Collection books), but it doesn’t store if the author wrote the whole book or drew the illustrations. What is the best way to solve that?

  6. I was just wondering how you would get to the intersection table attribute Contributor Type.

  7. Great writeup. One thing I’d like to mention though is the merge/refresh functionality. Refresh is only to be used on Managed entities, where as merge is used on detatched entities and will return the managed instance with the changes merged into it. So to avoid the exception, your code really should be:

    managedJsf = libraryService.getEntityManager().merge(jsf);
    libraryService.getEntityManager().refresh(managedJsf );

    Though it might be better just to call find and then refresh on the returned entity.

    Best Regards

  8. The ER Diagram in this article was created using Oracle Designer. The Server diagram showing the tables was done with Oracle JDeveloper. Thanks for your kind words!

    regards,

    Lucas

  9. I am reading with interest your EJB3 articles. Thanks!
    Which tool are you using to create the ER diagrams present in this article?