<--back

EJB3 Tutorial 3 - A Mini Application


This tutorial revisits JPA Tutorial 3 and migrates it from a JSE Solution to a JEE solution. Along the way, we're going to notice several things that we "missed". These were either things that the JSE environment let slide or things we missed because of how we were testing our solutions.

Background

A key difference between what we did and what we're going to do is transactional demarcation. In our JSE environment, we had a @Before method that started a transaction and an @After method that rolled the transaction back. This meant that multiple messages to, for example, the library, all happened in the same transaction. We can get similar behavior to this using EJB3 Session Beans with Extended Persistence Contexts. However, we're going to stick with Stateless Session Beans using transaction-scoped persistence contexts.

By default, each outer-most execution of a session bean method will:
  • Initialize the persistence context
  • Start a transaction
  • Execute the method
  • Commit the transaction
  • Clear the persistence context

If, within a session bean, the code calls another session bean, the following happens:
  • Work with existing persistence context
  • Join existing transaction
  • Execute the method

With a regular persistence context, committing a transaction causes everything in the persistence context to be flushed. That means two things:
  • Objects are no longer managed (they are detached)
  • Lazily-initialized relationships can no longer be traversed

If you use an extended persistence context, closing the transaction does not clear the persistence context. We will look at this further in a later tutorial. For now, we're sticking with the basics.

Setting up the Project

First we need to start with a project. Rather than having to copy all of JPA Tutorial 3, instead use the following 7-zip file: . You are welcome to use your version of Jpa Tutorial 3, however if you do these instructions might not match your experience.

Note that this file already has a conf directory as described in EJB3 Tutorial 1 - Create and Configure and the classpath is already already set.

  1. Extract this file using 7-zip. Place the contents of this archive under your workspace directory. For example, if your workspace directory is c:\workspaces\Ejb3JpaTutorials, after extracting the contents of this archive, you'll have a new directory named c:\workspaces\Ejb3JpaTutorials\Ejb3Tutorial3.
  2. Next, import the project into your workspace
  3. Start eclipse and open your workspace directory (if you're already in Eclipse, you do not need to restart)
  4. Pull down the File menu and select import
  5. Expand General
  6. Select Existing Projects into Workspace
  7. Click Next
  8. Select root directory is already selected, click on Browse
  9. Select the directory you created when you extracted the archive (c:\workspaces\Ejb3JpaTutorials\Ejb3Tutorial3)
  10. Click OK
  11. Click Finish

Verify that everything compiled successfully. Once you've fixed any compilation problems, run the unit tests. You might notice a few warnings and even some Fatal logging statements, but the tests should pass. As we migrate this solution to use EJB's, these errors will eventually go away based on how we change our test setup.

Database Configuration

persistence.xml

As we have seen with the previous EJB tutorials, the persistence.xml looks a little different for a JEE environment. Update the persistence.xml to resemble the following:
persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence>
   <persistence-unit name="lis">
      <jta-data-source>java:/HypersonicLocalServerDS</jta-data-source>
      <properties>
         <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
         <property name="hibernate.show.sql" value="true"/>
      </properties>
   </persistence-unit>
</persistence>

Data Source and Database

This persistence.xml makes use of a data source that we mentioned here. We're using this so that we have a database we can look at as we work through our tests to make sure we're cleaning everything up properly.

If you're working with a preconfigured system, the startdb.bat file mentioned below will already exist, you just need to run it.

The files you downloaded already contained changes in support of the hypersonic local server data source definition. You'll still need to start hypersonic. To do so, you can do the following:
  1. Change to the directory where you installed hsqldb (c:\libs\hsqldb)
  2. Make a new directory, called databases
  3. Change to that directory
  4. Start the database (requires you can execute a Java VM from the command line)
java -cp ../lib/hsqldb.jar org.hsqldb.Server -database.0 file:mydb -dbname.0 xdb


First EJB

Here is a list of the classes we'll convert to Session Beans:
  • BookDao
  • Library
  • LoanDao
  • PatronDao
  • ResourceDao

What about naming conventions? Every Session Bean has at least one interface and one class. We need to pick a name for the interface and the class. One convention is to add "Bean" after the name of the interface. So if we had an interface called RepairFacility, then the implementation would be called RepairFacilityBean. We'll use this convention and end up with the following names:
Original
Interface
Bean
BookDao
BookDao
BookDaoBean
Library
Library
LibraryBean
LoanDao
LoanDao
LoanDaoBean
PatronDao
PatronDao
PatronDaoBean
ResourceDao
ResourceDao
ResourceDaoBean

We are going to have to rename each of our beans and then create an interface. Luckily Eclipse will take care of most of this for us.

Rename


We'll start with PatronDao. To Rename it:
  1. Select PatronDao in the Package Explorer
  2. Right-click, select Refactor:Rename
  3. Enter PatronDaoBean for the name
  4. Press OK

Make it a Stateless Session Bean


We need to annotate this class to declare it is a session bean. Annotate the class with @Stateless.

Extract Interface


Next, we need to extract the interface automatically:
  1. Select PatronDaoBean in the Package Explorer
  2. Right-click, select Refactor:Extract Interface
  3. Enter PatronDao for the interface name
  4. Select all of the methods
  5. Make sure to select Use the extracted interface type where possible
  6. Optionally deselect Declare interface methods 'public'
  7. Optionally deselect Declare interface methods 'abstract'
  8. Click OK

Update Unit Test: PatronDaoTest


The unit test inherits from BaseDbDaoTest. This adds support for creating dao's entity managers, etc. We don't want to do any of this, so we can safely remove the base class.

We need to updated getDao() in the following ways:
  • It no longer should have the @Override annotation
  • It simply uses the JBossUtil to look-up the dao
  • Make sure to remove the dao instance variable and fix any resulting compilation errors by replacing dao with getDao()

Here is an updated version of that method:
    public PatronDao getDao() {
        return JBossUtil.lookup(PatronDao.class, "PatronDaoBean/local");
    }
Notice two things about the name we provide. First, we use the unqualified name of the bean class, PatronDaoBean. Also notice we need to add /local. If you had a remote interface, you'd instead use /Remote. And if you leave this off, you'll get a bad cast exception. You might experiment with this to discover why.

We also need to initialize the EJB Container. Add the following method:
    @BeforeClass
    public static void initContainer() {
        JBossUtil.startDeployer();
    }

Finally, since we are now testing PatronDaoBean instead of PatronDao, we might want to rename the test to PatronDaoBeanTest.java.

Run the PatronDaoBeanTest Tests: First Failure


When you run the unit tests (just PatronDaoBeanTest), they will all fail. If you review the stack trace in the JUnit window, you'll notice that all the lines that fail look something like this:
        getEm().persist(p);
We're getting a null pointer exception on this line because getEm() returns null. There was a method with the @Before annotation that set the entity manager on the PatronDao. We no longer inherit from that class so we no longer get that initialization. However, this is not how we should be initializing that attribute anyway. We can use the container to perform this initialization.

We can have the container inject the entity manager (or an entity manager factory if you'd like) into our PatronDaoBean.

To get an entity manager injected, we use the annotation @PersistenceContext on an attribute of type EntityManager. Since we inherit the entity manager attribute from a base class, we place that annotation in the base class, BaseDao, as follows:
public abstract class BaseDao {
    @PersistenceContext(unitName = "lis")
    private EntityManager em;
    ...
}

Using the @PersistenceContext will tell the container to look up the named EntityManagerFactory, create an EntityManager for us and then place that entity manager into the variable, in this case em. This happens when we look up the PatronDao.

Execute the PatronDaoBeanTest tests only -- if you were to run all the tests in the project you would get misleading errors at this point ("could not insert [entity.Address]").

Second Attempt: Second Failure


After adding in @PersistenceContext we get one test to pass and three to fail. If you look at the stack trace in the JUnit window, we see that the Patron class does not have a default constructor. We need to add a default constructor to Patron.java:
    public Patron() {
    }

Add it and re-run the tests (again, just PatronDaoBeanTest).

Go back and do the steps you just did for all the dao classes



Success

This fixes all the tests in PatronDaoBeanTests. For some reason when we ran this code in a JSE environment, it "worked" even though it did not comply with the standard. Overall this first conversion was fairly painless. However, before we go on, are out tests isolated? That is, after we execute the tests did we remember to remove everything we created?

Review: Are We Isolated?

At this point we need a tool to review the contents of the data base. If you want to work directly in Eclipse, you can use Quantum DB. If you prefer to work outside of Eclipse (and frankly with a more powerful tool), then you might want to try SQuirrel Sql Client.

If you start with a clean database and run the tests, 2 patrons are left in the database after the tests execute.

This means that the tests leave a foot print. Or they are not isolated. We want our tests to have no side-effects because they might run in any order and such side effects could cause other tests to fail. In the old way of doing things, we started a transaction and then rolled it back, so nothing got saved to the database. We have three options on how to avoid this:
  1. Try to simulate the old behavior
  2. Create a new database every time
  3. Clean up after ourselves

The first option is tricky at best. We've already seen that we missed some things in a JSE environment and, more importantly, this is not how our system will be running so even if we the first option to work, we've not really improved anything.

The second option is good but it has a few flaws:
  1. It simply masks bad tests and unless we drop the database after every test, we have not solved any problem.
  2. What if we want to run our tests against a populated database? We could re-create the database, but we'd still have to do it after every test.

Really, the best option is to write our tests so they clean up after themselves.

There are 4 tests. One is to test removing a Patron so it runs clean. Another looks up a Patron with a bogus key, so it does not have any side effects. This leaves the following two tests we need to fix:
  • createAPatron
  • updateAPatron

Here are the updates to PatronDaoBeanTest:
    @Test
    public void createAPatron() {
        final Patron p = createAPatronImpl();
 
        try {
            final Patron found = getDao().retrieve(p.getId());
            assertNotNull(found);
        } finally {
            removePatron(p);
        }
    }
 
    @Test
    public void updateAPatron() {
        final Patron p = createAPatronImpl();
 
        try {
            final String originalPhoneNumber = p.getPhoneNumber();
            p.setPhoneNumber(NEW_PN);
            getDao().update(p);
            final Patron found = getDao().retrieve(p.getId());
 
            assertNotNull(found);
            assertFalse(NEW_PN.equals(originalPhoneNumber));
            assertEquals(NEW_PN, p.getPhoneNumber());
        } finally {
            removePatron(p);
        }
    }
 
    private void removePatron(final Patron p) {
        getDao().removePatron(p.getId());
    }

We used a try {} finally block to make sure after the test finishes that we call a support method, removePatron. The fix is trivial (so far). We also added a simple private method to actually perform the delete.

EJB 2: ResourceDao

Update Dao

  1. Rename ResourceDao --> ResourceDaoBean
  2. Add @Stateless annotation to ResourceDaoBean
  3. Extract interface ResourceDao from ResourceDaoBean

Update Test

  1. Rename ResourceDaoTest --> ResourceDaoBeanTest
  2. Remove Base Class
  3. Remove dao attribute
  4. Rewrite getDao() to return a looked up ResourceDao
  5. Add method with @BeforeClass annotation that initializes the container

Run Your Tests (ResourceDaoBeanTest): First Failures

After making these changes, we have 3 tests that pass and one that fails. The error in the JUnit stack trace looks like this: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: entity.Book.authors, no session or session was closed

Here's the actual test:
01:    @Test
02:    public void updateABook() {
03:        final Book b = createABookImpl();
04:        final int initialAuthorCount = b.getAuthors().size();
05:        b.addAuthor(new Author(new Name("New", "Author")));
06:        getDao().update(b);
07:        final Resource found = getDao().retrieve(b.getId());
08:        assertTrue(found instanceof Book);
09:        assertEquals(initialAuthorCount + 1, ((Book) found).getAuthors().size());
10:    }

If you double-click on the last line listed in the stack trace, it will show the last line, line 9, as the problem line. When we retrieve the authors() from found, there's no problem -- yet. When we ask the object returned from getAuthors() for its size(), we're accessing not a collection but a proxy to a collection. Remember that the object found is detached. Why? We are no longer in a method in the container, we have returned from the method. When we returned, the transaction committed and all objects in the persistence context were detached. The author’s relationship is lazily loaded by default (because it is a @ManyToMany and that's the default behavior).

We have three ways to fix this problem:
  1. Use Eager fetching
  2. Directly access the collection while still in the session bean method to get it initialized (read in)
  3. Change the test to send a message to the ResourceDao to ask for the number of authors associated with a particular book

We'll take the easiest way out to fix this and make this relationship eagerly fetched rather than lazily fetched. Here's the change to Book.java:
    @ManyToMany(mappedBy = "booksWritten", cascade = { CascadeType.PERSIST,
            CascadeType.MERGE }, fetch = FetchType.EAGER)
    private Set<Author> authors;

The change there is adding fetch = FetchType.EAGER

Try #2

When you run this, things still do not work. This is a bi-directional relationship. However, while we are adding an author to the book, we are not adding the book to the author. Remember that we must maintain both sides of a bi-directional relationship. Update the addAuthor() method in book to add the book to the author:
    public void addAuthor(final Author author) {
        getAuthors().add(author);
        author.addBook(this);
    }

Run the tests in ResourceDaoBeanTest. Now more tests are failing. This is getting worse before it gets better.

Nearly Finally Fixed


There's a method in Author.java that looks like this:
    public void addBook(final Book b) {
        booksWritten.add(b);
        b.addAuthor(this);
    }

The problem is, the booksWritten attribute is never assigned. Here's a way to fix this:
  1. Change booksWritten to getBookWritten()
  2. Lazily initialize booksWritten

    public void addBook(final Book b) {
        getBooksWritten().add(b);
    }
 
    public Set<Book> getBooksWritten() {
        if(booksWritten == null) {
            booksWritten = new HashSet<Book>();
        }
 
        return booksWritten;
    }

Finally All of ResourceDaoBaseTest Running


We're close. The problem is since we started properly maintaining a bi-directional relationship between books and authors; JPA is automatically inserting two foreign keys in to a join table called AUTHOR_BOOK. Well to remove the book, we need to remove the relationship between the book and the authors (both sides).

Here is the challenge. We have one dao, ResourceDao, which removes both books and dvd's as well as all kinds of resources. The book has a dependency on Author that not all resources have. So how can we still use the ResourceDao to remove a book if the book has specific logic? Here are two options:
  1. Use type-checking in the ResourceDao to do custom delete logic based on type
  2. Have the resource dao delegate a message to all resources polymorphically, the book will take care of specific clean-up logic

Type checking is not always bad, just mostly always. We won't even consider that because polymorphism is the way to go here. Here are the three steps we're going to follow:
  1. Create an abstract method in Resource called remove().
  2. Add an empty implementation of this method to Dvd so it will compile
  3. Add an implementation into Book to clean up all of its relationships
  4. Add any required supporting methods in other classes
  5. Make sure to actually call the remove method in the ResourceDao

Add abstract method to Resource
    public abstract void remove();

Add empty implementation to Dvd
    @Override
    public void remove() {
    }

Add implementation to Book
    @Override
    public void remove() {
        for (Author a : getAuthors()) {
            a.removeBook(this);
        }
        getAuthors().clear();
    }

Add removeBook to Author
    public void removeBook(final Book book) {
        getBooksWritten().remove(book);
    }

Call the remove() method in ResourceDao
Just after looking up the resource and just before actually removing it, we need to call the remove() method on the Resource object:
    public void remove(Long id) {
        final Resource r = retrieve(id);
        if (r != null) {
            r.remove();
            getEm().remove(r);
        }
        getEm().flush();
    }
Run the tests and they now all pass.

Test Isolation


Now that all of our tests in ResourceDaoBeanTest pass, we need to clean up after ourselves. Here are the stats:
Table
  1. Rows
Author
7
Book
2
Author_Book
5
Resource
2

We need to update each of the tests that create books and explicitly remove the books and authors created. It turns out we do not need to explicitly remove anything from Author_Book. Just updating the bi-directional relationships properly will fix that problem.

Delete Author

To delete authors, we'll create a new Dao for Authors:
AuthorDao
package session;
 
import java.util.Set;
 
import entity.Author;
 
public interface AuthorDao {
    void remove(final Set<Author> authors);
}

AuthorDaoBean
package session;
 
import java.util.Set;
 
import javax.ejb.Stateless;
 
import entity.Author;
import entity.Book;
 
@Stateless
public class AuthorDaoBean extends BaseDao implements AuthorDao {
    public void remove(final Set<Author> authors) {
        for (Author a : authors) {
            final Author toDelete = getEm().find(Author.class, a.getId());
            for (Book b : toDelete.getBooksWritten()) {
                b.getAuthors().remove(toDelete);
            }
            toDelete.getBooksWritten().clear();
            getEm().flush();
            getEm().remove(toDelete);
        }
    }
}

Update Test

Support Methods
First we need a few methods we can use to delete authors and books (we're working in ResourceDaoBeanTest, so this is a fine place to add these methods):
    public void removeAuthors(final Set<Author> authors) {
        JBossUtil.lookup(AuthorDao.class, "AuthorDaoBean/local")
                .remove(authors);
    }
 
    public void removeBookAndAuthors(final Book book) {
        final Book b = (Book) getDao().findById(book.getId());
        getDao().remove(b.getId());
        removeAuthors(book.getAuthors());
    }

These methods are static because we might want to use them from other tests. However, to make them static, we must change getDao() to be a static method as well.

Updated Tests
Here are the updated tests that now use the support methods.
    @Test
    public void createABook() {
        final Book b = createABookImpl();
        try {
            final Resource found = getDao().retrieve(b.getId());
            assertNotNull(found);
        } finally {
            removeBookAndAuthors(b);
        }
    }
 
    @Test
    public void removeABook() {
        final Book b = createABookImpl();
        try {
            Resource found = getDao().retrieve(b.getId());
            assertNotNull(found);
            getDao().remove(b.getId());
            found = getDao().retrieve(b.getId());
            assertNull(found);
        } finally {
            removeAuthors(b.getAuthors());
        }
    }
 
    @Test
    public void updateABook() {
        Book b = createABookImpl();
 
        try {
            final int initialAuthorCount = b.getAuthors().size();
            b.addAuthor(new Author(new Name("New", "Author")));
            getDao().update(b);
            b = (Book) getDao().retrieve(b.getId());
            assertEquals(initialAuthorCount + 1, b.getAuthors().size());
        } finally {
            removeBookAndAuthors(b);
        }
    }

Notice that we re-assign the variable b just before the assert equals and it is that updated version of b that is sent to removeBookAndAuthors(). Why do you suppose we need to do that?

At this point you might want to go back and verify that the tests in PatronDaoBeanTest still pass.

Finish Conversion

We have the following classes to convert:
  • LoanDao
  • BookDao
  • Library

There's only one test left to convert, LibraryTest. We'll perform all of these conversions at once and see what we end up with (it won't be pretty).

BookDao
  1. Rename BookDao --> BookDaoBean
  2. Add @Stateless annotation to BookDaoBean
  3. Extract interface BookDao from BookDaoBean

LoanDao
  1. Rename LoanDao --> LoanDaoBean
  2. Add @Stateless annotation to LoanDaoBean
  3. Extract interface LoanDao from LoanDaoBean

Library
  1. Rename Library --> LibraryBean
  2. Add @Stateless annotation to LibraryBean
  3. Extract interface Library from LibraryBean
  4. Use @EJB to have the dao's injected

Here's the top of LibraryBean:
@Stateless
public class LibraryBean implements Library {
    @EJB
    private ResourceDao resourceDao;
    @EJB
    private BookDao bookDao;
    @EJB
    private PatronDao patronDao;
    @EJB
    private LoanDao loanDao;
    // ...
}

LibraryTest
  1. Rename LibraryTest --> LibraryBeanTest
  2. Remove Base Class from LibraryBeanTest
  3. Update setupLibrary to simply lookup the library and set the library attribute.
  4. Add method with @BeforeClass annotation that initializes the container

Changes to LibraryBeanTest:

We need to change how LibraryBeanTest sets itself up. Currently it has one @Before method and one @BeforeClass method. Ultimately we will have one @Before method and two @BeforeClass methods.

We need to change from this:
    @Before
    public void setupLibrary() {
        final ResourceDaoBean rd = new ResourceDaoBean();
        rd.setEm(getEm());
        final PatronDaoBean pd = new PatronDaoBean();
        pd.setEm(getEm());
        final LoanDao ld = new LoanDao();
        ld.setEm(getEm());
        final BookDao bd = new BookDao();
 
        library = new Library();
        library.setResourceDao(rd);
        library.setPatronDao(pd);
        library.setLoanDao(ld);
        library.setBookDao(bd);
    }
 
    @BeforeClass
    public static void setupDates() {
        Calendar c = Calendar.getInstance();
        DateTimeUtil.removeTimeFrom(c);
        CURRENT_DATE = c.getTime();
        c.add(Calendar.DAY_OF_MONTH, 6);
        CURRENT_PLUS_6 = c.getTime();
        c.add(Calendar.DAY_OF_MONTH, 2);
        CURRENT_PLUS_8 = c.getTime();
        c.add(Calendar.DAY_OF_MONTH, 6);
        CURRENT_PLUS_14 = c.getTime();
        c.add(Calendar.DAY_OF_MONTH, 1);
        CURRENT_PLUS_15 = c.getTime();
    }
 

To the following:
    @Before
    public void setupLibrary() {
        library = JBossUtil.lookup(Library.class, "LibraryBean/local");
    }
 
    @BeforeClass
    public static void initContainer() {
        JBossUtil.startDeployer();
    }
 
    // Note, the following method is unchanged
    @BeforeClass
    public static void setupDates() {
        Calendar c = Calendar.getInstance();
        DateTimeUtil.removeTimeFrom(c);
        CURRENT_DATE = c.getTime();
        c.add(Calendar.DAY_OF_MONTH, 6);
        CURRENT_PLUS_6 = c.getTime();
        c.add(Calendar.DAY_OF_MONTH, 2);
        CURRENT_PLUS_8 = c.getTime();
        c.add(Calendar.DAY_OF_MONTH, 6);
        CURRENT_PLUS_14 = c.getTime();
        c.add(Calendar.DAY_OF_MONTH, 1);
        CURRENT_PLUS_15 = c.getTime();
    }

While we're at it, we are no longer using the base classes so we can delete the following classes:
  • BaseDbDaoTest
  • EntityManagerBasedTest

Run LibraryBean test and things look a bit bleak. Out of 20 tests we have 8 errors and 9 failures. On the other hand, three tests passed successfully so it's not all bad.

Fixing The Tests


addBook

The last line of the addBook method fails. After a little research it turns out that the book's authors does not appear to contain all of the authors. If we step through all of this, it turns out that it does not contain any.

Here's a fact about the containAll() method on collections. It requires a proper definition of equals() and/or hashCode() depending on the type of collection. While Author has both hashCode and equals, both of these methods depend on Name.equals() and Name.hashCode(), neither of which are defined. So we need to add these missing methods to fix this problem.

We need to add the following methods to Name.java:
    public boolean equals(final Object object) {
        if (object instanceof Name) {
            final Name rhs = (Name) object;
            return rhs.getFirstName().equals(getFirstName())
                    && rhs.getLastName().equals(getLastName());
        }
        return false;
    }
 
    public int hashCode() {
        return getFirstName().hashCode() * getLastName().hashCode();
    }

Run the test and after making this change, you'll notice that addBook passes.

lookupBookThatDoesNotExist

When a method on a session bean throws an exception it will either return wrapped in an EJBException or "raw" depending on if the exception has the annotation @ApplicationException. The method findResourceById currently uses EntityNotFoundException, but we don't own that exception so we will make our own exception class and throw it instead.

Here's a new exception:
EntityDoesNotExist Exception
package exception;
 
import javax.ejb.ApplicationException;
 
@ApplicationException
public class EntityDoesNotExist extends RuntimeException {
    private static final long serialVersionUID = 2838964492920113727L;
 
    private final Class clazz;
    private final Object key;
 
    public EntityDoesNotExist(final Class clazz, final Object key) {
        this.clazz = clazz;
        this.key = key;
    }
 
    public Class getClazz() {
        return clazz;
    }
 
    public Object getKey() {
        return key;
    }
}

Now we need to update two things:
  1. Update the method to throw this new exception
  2. Change the (expected = ) clause of the unit test

Here's the updated method in LibraryBean:
    public Resource findResourceById(Long id) {
        final Resource r = getResourceDao().findById(id);
        if (r == null) {
            throw new EntityDoesNotExist(Resource.class, id);
        }
        return r;
    }

And the updated test method:
    @Test(expected = EntityDoesNotExist.class)
    public void lookupBookThatDoesNotExist() {
        library.findResourceById(ID_DNE);
    }

lookupPatronThatDoesNotExist

The test suffers from the Same problem as the above example. Do the same thing.

checkoutBook

After digging into this problem a bit, you'll discover that Patron is missing equals() and hashCode():
    @Override
    public boolean equals(final Object rhs) {
        return rhs instanceof Patron
                && EqualsUtil.equals(getId(), ((Patron) rhs).getId());
    }
 
    @Override
    public int hashCode() {
        return getId().hashCode();
    }

returnBook

There are two problems with this test. First, we're using detached objects after they have been updated. Second, there's a lazily-initialized relationship. We'll fix the relationship first and the re-write the test to perform some additional lookups.

            library.checkout(p.getId(), CURRENT_DATE, b.getId());
 
            Patron foundPatron = library.findPatronById(p.getId());
            final int resourcesBefore = foundPatron.getCheckedOutResources()
                    .size();
 
            Resource foundBook = library.findResourceById(b.getId());
            assertTrue(foundBook.isCheckedOut());
            library.returnResource(CURRENT_PLUS_8, foundBook.getId());
            foundPatron = library.findPatronById(p.getId());
            assertEquals(resourcesBefore - 1, foundPatron
                    .getCheckedOutResources().size());
            foundBook = library.findResourceById(b.getId());
            assertFalse(b.isCheckedOut());
            assertEquals(0, p.getFines().size());

Once we make these changes and re-run the test, we get the following exception:
java.lang.RuntimeException: org.jboss.tm.JBossRollbackException:
Unable to commit, tx=TransactionImpl:XidImpl[FormatId=257, GlobalId=null:
1164776892890/6, BranchQual=null:1164776892890, localId=0:6], status=
STATUS_NO_TRANSACTION; - nested throwable: (javax.persistence.EntityNotFoundException:
deleted entity passed to persist: [entity.Loan#<null>])

OK, what does this mean? After some researching and guessing, you'll discover that this probably means you are trying to delete some object and doing so violates a foreign key constraint. It mentions Loan. If you do a little more digging, you'll find out that when you try to remove a loan from a collection of loans in a Patron, the loan is not removed. Why? No equals() or hashCode(). Here they are:
    @Override
    public boolean equals(final Object rhs) {
        return rhs instanceof Loan
                && ((Loan) rhs).getPatronId().equals(getPatronId())
                && ((Loan) rhs).getResourceId().equals(getResourceId());
    }
 
    @Override
    public int hashCode() {
        return getResourceId().hashCode() * getPatronId().hashCode();
    }

We need to make two more updates to get rid of this foreign key constraint.

Update LoanDaoBean
    public void remove(final Loan l) {
        l.remove();
        getEm().flush();
        getEm().remove(l);
    }

Update Loan
    public void remove() {
        getPatron().removeLoan(this);
        getResource().setLoan(null);
    }

One Final Change
Here's one more thing that has to do with how JPA reads JoinTables. In the case of our Loan join table, it will read two records for each one record. (Insert reference as to why.) There is an easy fix. In the Patron we store a List<Loan>, change this to Set<Loan> and update all of the related code in Loan get it to compile. There are two kinds of replacements you'll have to make:
  • Replace all occurrences of List<Loan> with Set<Loan>
  • Replace all occurrences of ArrayList<Loan>() with HashSet<Loan>()

Finally, run the test to verify that it now works.

returnResourceLate

We have three problems with this test:
  • Detached Object
  • Lazy relationship
  • Using List where we should use a Set

To fix the detached object problem, look up the patron after returning the resource and just before the asserts.

To fix the lazy relationship, add fetch=FetchType.EAGER to the fines attribute.

To fix the List<Fine>, replace all List<Fine> with Set<Fine> and also replace all ArrayList<Fine>() with HashSet<Fine>().

returnResourceThatsNotCheckedOut

We are throwing an exception, ResourceNotCheckedOut, that has not had the @ApplicationException annotation added to it.

checkoutBookThatIsAlreadyCheckedOut

Same problem as with the previous test.

checkoutBookThatDoesNotExist

We should replace EntityNotFoundException with EntityDoesNotExist Exception.

checkoutBookToPatronThatDoesNotExist

Same problem as the previous test.

findOverdueBooks

This test is actually failing because of previous tests. Since we have not made our tests isolated, we cannot really fix this test. However, we can verify that this test is not broken. Clean up the database and run this test to verify that it works.

Here's the order in which you can drop all records from the database:
  • author_book
  • patron_fine
  • fine
  • author
  • book
  • loan
  • patron
  • dvd
  • director
  • book
  • resource
  • address

There are other orders you could use, but this one works.

If you'd like to add a temporary method to your test class to clean up after each test, here is one that will do it:
    @After
    public void cleanupDatabase() {
        EntityManagerFactory f = Persistence.createEntityManagerFactory("lis");
        EntityManager em = f.createEntityManager();
        em.getTransaction().begin();
        em.createNativeQuery("delete from author_book").executeUpdate();
        em.createNativeQuery("delete from patron_fine").executeUpdate();
        em.createNativeQuery("delete from fine").executeUpdate();
        em.createNativeQuery("delete from author").executeUpdate();
        em.createNativeQuery("delete from loan").executeUpdate();
        em.createNativeQuery("delete from patron").executeUpdate();
        em.createNativeQuery("delete from dvd").executeUpdate();
        em.createNativeQuery("delete from director").executeUpdate();
        em.createNativeQuery("delete from book").executeUpdate();
        em.createNativeQuery("delete from resource").executeUpdate();
        em.createNativeQuery("delete from address").executeUpdate();
        em.getTransaction().commit();
    }
Notes
Additional Jar
To get this to work, you'll need to add an optional library to your classpath:
  • ehcache-1.2.jar

If you've used the same directories as these instructions, you'll find the file here:
  • C:\libs\jboss-EJB-3.0_Embeddable_ALPHA_9\optional-lib

Possible Reordering
Also, if you managed to fix the OneToOne, the order from above changes. Move dvd, directory book and resource before loan.

This Is a Temporary Fix
Note, once we work on making each of our tests isolated, we'll need to remove this method. And this method makes it impossible to look at the contents of the database after running the tests. It also slows things down and would not work with a pre-populated database. So this really is temporary scaffolding until we can get to the next phase of cleaning up properly after each test.

patronsWithOverdueBooks

Same problem as above.

payFineInsufficientFunds

InsufficientFunds needs to be an application exception.

patronCannotCheckoutWithFines

PatronHasFines class should be an application exception.

checkoutDvd

This is a detached object problem. After the call to checkout and before the asserts, make sure to get a fresh version of the dvd.

returnDvdLate

This is a detached object problem. You need to update both the patron and the dvd before the asserts.

checkoutDvdAndBook

This is a detached object problem. You need to update both the dvd and the book before the asserts.

Test Isolation

Finally, we need to make our test clean up after themselves. Along the way we're going to have to make a few big changes to make all of this work. We'll clean up each test one after the other.

addBook

This one is straightforward. We can use the method removeBookAndAuthors in the ResourceDaoBeanTest:
    @Test
    public void addBook() {
        final Book b = createBook();
 
        try {
            final Set<Author> authors = b.getAuthors();
            final Resource found = library.findResourceById(b.getId());
 
            assertTrue(found instanceof Book);
            assertTrue(((Book) found).getAuthors().containsAll(authors));
        } finally {
            ResourceDaoBeanTest.removeBookAndAuthors(b);
        }
    }

To test this, make sure your database is clean. Next, comment out or delete the cleanupDatabase method (and make sure to get the annotation). Run this test by itself and verify that nothing remains in the database after executing the test.

lookupBookThatDoesNotExist

This test creates no objects so no cleanup is necessary.

addPatron

We have a method in PatronDaoBeanTest that we could use, but we need to make two changes:
  1. Make the method PatronDaoBeanTest.removePatron public and static
  2. Make the metho PatronDaoBeanTest.getDao() static

Once you've done that, you can change the test:
    @Test
    public void addPatron() {
        final Patron p = createPatron();
        try {
            final Patron found = library.findPatronById(p.getId());
            assertNotNull(found);
        } finally {
            PatronDaoBeanTest.removePatron(p);
        }
    }

lookupPatronThatDoesNotExist

This test creates no objects so no cleanup is necessary.

checkoutBook

When we checkout a book, we create a loan. So in addition to removing the two books and patrons that are created as a result of this test, we must also remove the loan.

This one requires a bit more work. First the updated test:
    @Test
    public void checkoutBook() {
        final Book b1 = createBook();
        final Book b2 = createBook();
        final Patron p = createPatron();
 
        try {
            library.checkout(p.getId(), CURRENT_DATE, b1.getId(), b2.getId());
 
            final List<Resource> list = library
                    .listResourcesOnLoanTo(p.getId());
 
            assertEquals(2, list.size());
 
            for (Resource r : list) {
                assertTrue(r.isOnLoanTo(p));
                assertTrue(r.dueDateEquals(CURRENT_PLUS_14));
            }
        } finally {
            library.removePatron(p.getId());
            ResourceDaoBeanTest.removeBookAndAuthors(b1);
            ResourceDaoBeanTest.removeBookAndAuthors(b2);
        }
    }

The finally block uses a method Library.removePatron that is new. We need to add it both to the Library interface and provide an implementation for this method in the LibraryBean:
    public void removePatron(Long id) {
        final Patron found = findPatronById(id);
 
        final Set<Loan> loans = found.getCheckedOutResources();
        found.setCheckedOutResources(null);
 
        for (Loan l : loans) {
            getLoanDao().remove(l);
        }
 
        final Set<Fine> fines = found.getFines();
        found.setFines(null);
 
        for (Fine f : fines) {
            removeFine(f);
        }
 
        patronDao.removePatron(id);
    }
 
    public void removeFine(final Fine f) {
        getResourceDao().removeFind(f);
    }

We also added the method ResourceDao.removeFine. We need to add it to the interface and to ResourceDaoBean:
    public void removeFind(final Fine f) {
        final Fine found = getEm().find(Fine.class, f.getId());
        if (found != null) {
            getEm().remove(found);
        }
    }

returnBook

Give the support for removing patrons, we can now use that in the returnBook test. Here's the skeleton:
        final Book b = createBook();
        final Patron p = createPatron();
 
        try {
 
            // ... unchanged ...
 
        } finally {
            library.removePatron(p.getId());
            ResourceDaoBeanTest.removeBookAndAuthors(b);
        }

returnResourceLate

This test can use the same skeleton as returnBook to clean up after itself.

returnResourceThatsNotCheckedOut

This test only needs to remove a book. Follow the skeleton from returnBook.

checkoutBookThatIsAlreadyCheckedOut

Remove the two Patrons then remove the book. Follow the skeleton from returnBook.

checkoutBookThatDoesNotExist

Remove the created patron. Follow the skeleton from returnBook.

checkoutBookToPatronThatDoesNotExist

Remove the created book. Follow the skeleton from returnBook.

findOverdueBooks

Remove the patron that is created then the two books. Follow the skeleton from returnBook.

patronsWithOverdueBooks

Remove the patron that is created then the two books. Follow the skeleton from returnBook.

calculateTotalFinesForPatron

Remove the patron that is created then the two books. Follow the skeleton from returnBook.

payFineExactAmount

Up to this point we were doing so well. Unfortunately, when we pay fines, we remove fines from our entities but we do not remove them properly. You can tell this by stepping through the code and the useful stack trace.

To fix this, we need to add just a bit of infrastructure. First the background. When we call Library.tenderFine(), a message goes to Patron. The patron removes fines from its collection based on the amount tendered and then returns the balance. Unfortunately, the fines removed from its collection need to be deleted. So we have two options:
  • The Patron entity uses some Dao to remove the Fine entities from the database
  • The Patron dao returns both the fines remove and the balance and lets the caller deal with the fined.

The first option potentially creates a circular dependency and also has and entity dealing with the database, which we have not had to do so far. We'll take option 2. Here are all the necessary changes.

FinesPaidAndBalance
package complexreturns;
 
import java.util.List;
 
import entity.Fine;
 
public class FinesPaidAndBalance {
    final private List<Fine> finesPaid;
    final private double balance;
 
    public FinesPaidAndBalance(final List<Fine> finesPaid, final double balance) {
        this.finesPaid = finesPaid;
        this.balance = balance;
    }
 
    public double getBalance() {
        return balance;
    }
 
    public List<Fine> getFinesPaid() {
        return finesPaid;
    }
}

Patron.pay
    public FinesPaidAndBalance pay(final double amountTendered) {
        double totalFines = calculateTotalFines();
        if (totalFines <= amountTendered) {
            List<Fine> finesPaid = new ArrayList<Fine>(getFines().size());
            finesPaid.addAll(getFines());
            getFines().clear();
            return new FinesPaidAndBalance(finesPaid, amountTendered
                    - totalFines);
        } else {
            throw new InsufficientFunds();
        }
    }

LibraryBean.tenderFine
    public double tenderFine(final Long patronId, double amountTendered) {
        final Patron p = getPatronDao().retrieve(patronId);
        final FinesPaidAndBalance finesPaid = p.pay(amountTendered);
 
        for (Fine f : finesPaid.getFinesPaid()) {
            removeFine(f);
        }
 
        return finesPaid.getBalance();
    }

payFineInsufficientFunds

Remove the created patron and book. Follow the skeleton from returnBook.

patronCannotCheckoutWithFines

Remove the created patron and book following the skeleton from returnBook.

checkoutDvd

Remove the patron following the skeleton from returnBook.

Your challenge is to somehow call the ResourceDao.remove() method passing in the id of the dvd. You'll also need to remove the director.

returnDvdLate

Remove the patron following the skeleton from returnBook.

Your challenge is to somehow call the ResourceDao.remove() method passing in the id of the dvd. You'll also need to remove the director.

checkoutDvdAndBook


Remove the patron and the book using the skeleton from returnBook.

Your challenge is to somehow call the ResourceDao.remove() method passing in the id of the dvd. You'll also need to remove the director.

FAQ

Q/A

  • @EJB Automatically filling in a dao. If the type of the Bean is unambigious, then JNDI will automatically insert your session bean reference.
  • Would local interface imply different semantics than remote? The strictness could be better for testing...fail faster.
  • Brett: Why use EJB3? class: security, easy web services, . brett: transaction demarcation, organizational mandate, standard (community+materials), entity beans done right (jpa), commercial support,
  • Could you explain injection again? A mechanism to implement Inversion of Control. An object is told how to get ahold of something it needs by setting the reference before it becomes active.
  • How is sun making any money?
  • Brett: Why should you use or not use stateful/stateless session beans? Stateless session beans: things you can fire and forget, lookups, etc. Stateful: Things where requirements dictate holding on to objects.
  • How do you hold on to the same stateful bean object across requests? Store the delegate/ref in the httpsession.

Take Aways

  • persistence.xml must be in right location or le be your butt.
  • Generated values might not make it back to your object if it runs outside the context of a transaction.
  • Merge returns a new object (unless the object is already managed)
  • Injection using @EJB
  • Use a set when possible (instead of list). Generally speaking, replace lists with collections
  • Name magic for mappedBy: Side with 'mappedBy' is the inverse side. Other side is the owner ('can exist alone'). * visual side discussion
  • How to know when something detached/attached.
  • Bi-directional relationships: how to properly delete (+verify it's cleaned up)
  • Try/catch/finally (in test) sometimes better than @Before/@After for certain init/cleanup
  • There's some value in having to suffer. (learning how to debug jpa issues)
  • Extended Context: use w/stateful beans, keeps the cache open after the end of transaction
  • **


<--back