Port of schuchert.wikispaces.com


JPA_Tutorial_3_A_Mini_Application

JPA_Tutorial_3_A_Mini_Application

JPA Tutorial 3 - A Mini Application

In this example we start with a simple domain model and incrementally migrate it to become more realistic. Along the way we end up using several features of JPA not yet covered by the previous tutorials.

The Problem


title: JPA_Tutorial_3_The_Problem — We want to implement a simple system to track books and eventually other resources for a library. This page covers the requirements for Version 1. The problem as stated is bigger than what this tutorial implements. Parts are left as exercises that are scheduled into a course or advanced exercises for students who finish their work early.

Checking out a Book

Description

A Patron checks out one or more books, all of which are due 14 days later.

Basic Course of Events

  • This use case begins when a Patron identifies him or herself by entering their Patron ID.
  • The system verifies that the Patron exists and is in good standing.
  • The system asks the Patron to enter the first resource.
  • The patron provides the book identifier (each book has a unique identifier because the ISBN is not enough, we need to know WHICH version of Catcher int the Rye the Patron is checking out)
  • The system records the book, calculates the due date and records the book as on loan to the Patron.
  • The use case ends.

Alternatives

Num Step Description
1 2 The Patron is not in good standing, they have overdue books. Do not allow them to check out any other books until they return all overdue books
2 2 The Patron is not in good standing, they have fines from overdue books they now paid. Do not allow them to check out any books until they pay their fines.
3 2 The Patron is not found (maybe their account was removed due to inactivity). Do not allow the to check out any books
4 5 The Book is not found, log the book and tell the Patron to ask for assistance with the particular book.
5 5 The Book is a reserve book and cannot be checked out. Inform the Patron.
6 5 The due date falls on a day when the Library is not open, move the return date to the next date the library is open.

Returning a Book

Description

A Patron returns a book. The library computes any fines and removes the recording of the loan of the book.

Basic Course of Events

  • This use case begins when a Patron returns a book. The Patron identifies the book by providing the book identifier.
  • The system retrieves the loan information for the book.
  • The system updates the book as being returned and indicates success to the Patron.
  • The patron indicates they are finished returning books.
  • The system reports the total of any fines for the books returned as well as any pending fines.
  • The system asks the user if they would like to pay their fines.
  • The user indicates the do: Initiate: Paying Fines
  • The use case ends.

Alternatives

Num Step Description
1 2 The book is not on loan, inform the user.
2 3 The book is late, calculate a fine and inform the user of a fine.
3 5 The user owes no fines, the use case ends.
4 6 The user indicates they do not want to pay fines right now. The system informs them they will not be able to checkout books until all fines are paid and the use case ends.

Adding a Book

Description

A Librarian wishes to add a new book into the system.

Basic Course of Events

  • The Librarian indicates they want to add a new Book.
  • The system asks the Librarian to provide book information (Book Title, Authors, ISBN, replacement cost)
  • The Librarian provides the book information.
  • The system validates the information.
  • The system creates a new book and assigns the book a unique identifier.
  • The system indicates the unique identifier for the book (and prints a book label)
  • The use case ends.

Alternatives

No alternatives listed for this use case.

Removing a Book

Description

The Librarian wishes to take a book out of the system and make it no longer available for checkout.

Basic Course of Events

  • The Librarian indicates they want to remove a book.
  • The system asks the librarian for the book identifier.
  • The Librarian provides the book identifier.
  • The system validates the book identifier and book.
  • The system removes the book from the system and indicates success.

Alternatives

Num Step Description
1 3 The book identifier is not found. Indicate the error. The use case ends.
2 4 The book is on loan. Remove the loan (ignoring any fines). Indicate the error to the user but complete the use case normally.
3 4 This is the last book with the that particular ISBN. Ask the user to confirm the removal. If confirmed, complete the use case normally. If not confirmed, do not remove the book. Either way, the use case ends.

Adding a Patron

Description

A Librarian adds a new Patron into the system.

Basic Course of Events

  • The Librarian indicates they wish to add a new Patron to the system.
  • The system asks the Librarian to provide the Name (first, mi, last), Address (street 1, street 2, city, state, zip), and Phone number(area code + 7 digits).
  • The system verifies the minimal information is provided.
  • The system creates a new Patron and assigned a Patron ID.
  • The system provides the new Patron ID back to the Librarian (and prints a card).
  • The use case ends.

Alternatives

Num Step Description
1 3 Some required information is missing. Indicate the required information and ask the Librarian to perform it. Continue back at step 2.

Removing a Patron

Description

The Librarian wants to remove a Patron.

Basic Course of Events

  • The Librarian indicates they want to remove a Patron.
  • The system asks for the Patron’s id.
  • The Librarian provides the id.
  • The system validates the id.
  • The system removes the Patron from the system.

Alternatives

Num Step Description
1 3 The id is not found. Indicate the error to the user and continue at step 2
2 3 The Patron has outstanding fines. Indicate this to the Librarian and ask to confirm the removal. If confirmed, remove and complete the use case normally. If not confirmed, end the use case without removing the Patron.
3 3 The Patron has outstanding loans. Indicate this to the Librarian and do not allow removal.

Paying Fines

Description

A Patron wishes to pay fines. Note that this use case can be invoked by itself or called from other use cases.

Basic Course of Events

  • A Patron is identified and their fines calculated.
  • The system asks for the amount tendered.
  • The system determines the difference and indicates the difference to the user.
  • The use case ends.

Alternatives

Num Step Description
1 1 The identified Patron has no fines. Indicate this to the user and the use case ends.
2 4 If there is still a balance, the system asks if it should ask for additional reimbursements. If yes, they go back to step 2, otherwise the use case ends.

Record a Book as Unrecoverable

Description

A book is not going to be returned/recovered. Add a fine if the book is on loan.

Basic Course of Events

  • This use case begins when a book is going to indicated as not returnable.
  • The system asks the user to provide the book id and a reason.
  • The user provides the id and reason.
  • The system retrieves the book.
  • The system calculates the replacement cost assigns it to the Patron who has it checked out.
  • The book is removed from the system.

Alternatives

Num Step Description
1 3 The book id is not known. Retrieve a list of books checked out to a Patron, select from the list and continue to step 3.
2 3 The book id is not known. Provide the isbn. Select the user who has the book checked out and select the book by id. Continue at step 3.
3 3 The book id is not known. Provide the title. Select the user who has the book checked out and select the book by id. Continue at step 3.
4 5 The book is not checked out, do not calculate any fines.

Reviewing all Checkouts for a Patron

Description

Report all of the books currently checked out by a Patron. Provide the title, isbn and due date. Sort by due date, with the book due the soonest at the beginning. If the user has an outstanding balance, indicate that as well.

Reviewing all Fines for all Patrons

Present a list of all the patrons with fines. Sort by the last name followed by the first name. Provide the name of the user, their phone number and their total balance.

V1 Project Setup


title: JPA_Tutorial_3_Project_Setup —

For the rest of this section, when you see ****, replace it with **JpaTutorial3**


title: JPA_Tutorial_Project_Setup —

Create Java Project

Next we need to create a Java project. We’ll keep the source separate from the bin directory:

  • Pull down the File menu and select New:Project
  • Select Java Project and click on Next
  • Enter a project name: ****, again read [this](JPA_Tutorial_1_Getting_Started#SideBarJpaClassPath) to know why I did not use a space in the project name.
  • Make sure “Create new project in workspace” is selected.
  • Make sure the JRE selected is 1.5.x or higher. If such a JRE does not show in the list, you can add it through Window->Preferences->JAVA->Installed JRE’s.
  • Select Create separate source and output folders
  • Click Finish

Create folders and packages

  • Expand your **** folder
  • Select the src directory
  • Right-click, select new:Folder
  • Use the name META-INF
  • Make sure **** is still selected, right-click and select **New:Source Folder**
  • Enter test and click OK

Add Required Libraries

We now need to add two libraries. Note that these steps assume you’ve already worked through the first tutorial and are working in the same workspace. If you, you’ll need to create user libraries. Review Creating User Libraries.

  • Edit the project properties. Select your **** and either press **alt-enter** or right-click and select properties.
  • Select Java Build Path
  • Click on the Libraries tab
  • Click on Add Library
  • Select User Libraries and click Next
  • Select JPA_JSE by click on the check box
  • Click OK
  • Click on Add Library again
  • Click on JUnit
  • Click Next
  • In the pull-down list, select JUnit 4
  • Click Finish
  • Click OK

If you’d like some background information on JUnit, please go here.

Configure Persistence Unit

<hr />

<p>title: JpaPersistenceUnit —</p>

<p id="Persistence_xml">We now need to create the Persistent Unit definition. We are going to create a file called persistence.xml in the src/META-INF directory with the following contents:</p>
<h3 id="persistencexml">persistence.xml</h3>

<figure class="highlight">
  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;pre&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span class="nt"&gt;&amp;lt;persistence&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;persistence-unit&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"examplePersistenceUnit"&lt;/span&gt; 
              &lt;span class="na"&gt;transaction-type=&lt;/span&gt;&lt;span class="s"&gt;"RESOURCE_LOCAL"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.show_sql"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.format_sql"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.connection.driver_class"&lt;/span&gt; 
              &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"org.hsqldb.jdbcDriver"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.connection.url"&lt;/span&gt; 
              &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"jdbc:hsqldb:mem:mem:aname"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.connection.username"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"sa"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.dialect"&lt;/span&gt; 
              &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"org.hibernate.dialect.HSQLDialect"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"hibernate.hbm2ddl.auto"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"create"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;/persistence-unit&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;/persistence&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt; </code></pre></div>      </div>
</figure>

<h3 id="the-steps">The Steps</h3>
<ul>
  • Expand your ****</li>

  • Select the src directory</li>

  • Find the src/META-INF directory (if one does not exist, right-click, select New:Folder, enter META-INF and press enter)</li>

  • Right click the src/META-INF, select new:File.</li>

  • Enter persistence.xml for the name and press “OK” (Note: all lowercase. It won’t make a difference on Windows XP but it will on Unix.)</li>

  • Copy the contents (above) into the file and save it</li> </ul> </div>

    </div>

    Update persistence.xml

    Update Persistence Unit Name

    The name of the persistence unit in your just-copied persistence.xml is examplePersistenceUnit in this example we use lis for Library Information System. Make the following change:

        <persistence-unit name="examplePersistenceUnit" 
                          transaction-type="RESOURCE_LOCAL">
        <persistence-unit name="lis" 
                          transaction-type="RESOURCE_LOCAL">

    Your project is now set up.

    </div>

    V1 First Test Suite


    title: JPA_Tutorial_3_V1_First_Test_Suite layout: default —

    The Unit Tests

    A Little Context

    Before we get started, this tutorial is deliberately organized in an inconvenient fashion. Why? My target reader is someone in a class I’m teaching (the material is open-source but still targeted). I do not want the student to be able to quickly just copy the whole thing and get it to work without having to put forth some effort. In a classroom situation, I have all of the source code available if I need to help a student get up to speed.

    We’ll start with a stripped down version of the requirements. This first test suite handles the following test cases:

    • Create a Patron
    • Remove a Patron
    • Update a Patron
    • Attempt to find Patron that does not exist.

    Notice that this suite of tests is for Creating, Reading, Updating and Deleting (CRUD) Patrons.

    Assuming you’ve done Tutorial 2, much of the boilerplate code is going to look the same. First let’s write a unit test for each of these test cases:

    Create a Patron

        @Test
        public void createAPatron() {
            final Patron p = createAPatronImpl();
            final Patron found = dao.retrieve(p.getId());
    
            assertNotNull(found);
        }
    
        private Patron createAPatronImpl() {
            final Address a = new Address("5080 Spectrum Drive", "Suite 700 West",
                    "Addison", "TX", "75001");
            return dao.createPatron("Brett", "Schuchert", "972-555-1212", a);
        }

    This test first creates a patron using a private utility method. This method exists because it is used later in other unit tests.

    Looking at the test, it uses an attribute called dao. This is a Data Access Object (which we’ll later convert to a stateless Session Bean). This Data Access Object will be responsible for retrieving, creating and removing Patrons.

    Remove a Patron

        @Test
        public void removeAPatron() {
            final Patron p = createAPatronImpl();
    
            dao.removePatron(p.getId());
            final Patron found = dao.retrieve(p.getId());
    
            assertNull(found);
        }

    This test uses the utility method to create a patron. It then removes it and makes sure that when we try to retrieve it that the Patron no longer exists.

    Update a Patron

        @Test
        public void updateAPatron() {
            final Patron p = createAPatronImpl();
    
            final String originalPhoneNumber = p.getPhoneNumber();
            p.setPhoneNumber(NEW_PN);
            dao.update(p);
            final Patron found = dao.retrieve(p.getId());
    
            assertNotNull(found);
            assertFalse(NEW_PN.equals(originalPhoneNumber));
            assertEquals(NEW_PN, p.getPhoneNumber());
        }

    We create a patron then update it.

    Attempt to find Patron that does not exist

        @Test
        public void tryToFindPatronThatDoesNotExist() {
            final Long id = -18128129831298l;
            final Patron p = dao.retrieve(id);
            assertNull(p);
        }

    Verify that when we try to find a patron that’s not found, we get back null.

    Supporting Code

    We have several veterans returning from previous tutorials. And here they are:

    PatronDaoTest

    First the imports and the attributes. Note that this is a complete class that will compile. It just doesn’t do anything yet.

    package session;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertFalse;
    import static org.junit.Assert.assertNotNull;
    import static org.junit.Assert.assertNull;
    
    import javax.persistence.EntityManager;
    import javax.persistence.EntityManagerFactory;
    import javax.persistence.Persistence;
    
    import org.apache.log4j.BasicConfigurator;
    import org.apache.log4j.Level;
    import org.apache.log4j.Logger;
    import org.junit.After;
    import org.junit.Before;
    import org.junit.BeforeClass;
    import org.junit.Test;
    
    import entity.Address;
    import entity.Patron;
    
    public class PatronDaoTest {
        private static final String NEW_PN = "555-555-5555";
        private EntityManagerFactory emf;
        private PatronDao dao;
    }

    Initialization of the Logger

    This is our 1-time initialization of the logging system.

        @BeforeClass
        public static void initLogger() {
            // Produce minimal output.
            BasicConfigurator.configure();
    
            // Comment this line to see a lot of initialization
            // status logging.
            Logger.getLogger("org").setLevel(Level.ERROR);
        }

    Getting EMF and EM

    Now before each unit test we’ll look up the entity manager factory, create a dao, create an entity manager and pass it into a DAO and finally start a transaction.

        @Before
        public void initEmfAndEm() {
            emf = Persistence.createEntityManagerFactory("lis");
            dao = new PatronDao();
            dao.setEm(emf.createEntityManager());
            dao.getEm().getTransaction().begin();
        }

    Clean up after each test

    After each test we’ll rollback the transaction we created in the pre-test initialization. We’ll then close both the entity manager and entity manager factory. This keeps our tests isolated.

        @After
        public void closeEmAndEmf() {
            dao.getEm().getTransaction().rollback();
            dao.getEm().close();
            emf.close();
        }

    The Entities

    We need to create entities. These entities are a bit more well-specified that what you’ve seen in the previous tutorials. In most cases I believe the extra information is intuitive. Where it is not, I’ll try to point out what is going on.

    Address.java

    package entity;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    
    @Entity  // The next class is a JPA entity
    public class Address {
        @Id // The next attribute (in this case) or method is a key field
        @GeneratedValue // the key is auto-generated
        private Long id;
        @Column(length = 50) // the next column is 50 characters in size
        private String streetAddress1;
        @Column(length = 50)
        private String streetAddress2;
        @Column(length = 20)
        private String city;
        @Column(length = 2)
        private String state;
        @Column(length = 9)
        private String zip;
    
        public Address() {
        }
    
        public Address(final String sa1, final String sa2, final String city,
                final String state, final String zip) {
            setStreetAddress1(sa1);
            setStreetAddress2(sa2);
            setCity(city);
            setState(state);
            setZip(zip);
        }
    
        public String getCity() {
            return city;
        }
    
        public void setCity(final String city) {
            this.city = city;
        }
    
        public String getState() {
            return state;
        }
    
        public void setState(final String state) {
            this.state = state;
        }
    
        public String getStreetAddress1() {
            return streetAddress1;
        }
    
        public void setStreetAddress1(final String streetAddress1) {
            this.streetAddress1 = streetAddress1;
        }
    
        public String getStreetAddress2() {
            return streetAddress2;
        }
    
        public void setStreetAddress2(final String streetAddress2) {
            this.streetAddress2 = streetAddress2;
        }
    
        public String getZip() {
            return zip;
        }
    
        public void setZip(final String zip) {
            this.zip = zip;
        }
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    }

    Patron.java

    package entity;
    
    import javax.persistence.CascadeType;
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.OneToOne;
    
    @Entity
    // the next class is a JPA entity
    public class Patron {
        @Id
        @GeneratedValue
        private Long id;
    
        // when in the database, this field cannot be null, as an object in memory
        // it can
        @Column(length = 20, nullable = false)
        private String firstName;
    
        @Column(length = 30, nullable = false)
        private String lastName;
    
        @Column(length = 11, nullable = false)
        private String phoneNumber;
    
        // This next field refers to an object that is stored in another table.
        // All updates are cascaded. So if you persist me, my address, which is in
        // another table, will be persisted automatically. Updates and removes are
        // also cascaded automatically.
        @OneToOne(cascade = CascadeType.ALL)
        private Address address;
    
        public Patron(final String fName, final String lName, final String phone,
                final Address a) {
            setFirstName(fName);
            setLastName(lName);
            setPhoneNumber(phone);
            setAddress(a);
        }
    
        public Address getAddress() {
            return address;
        }
    
        public void setAddress(Address address) {
            this.address = address;
        }
    
        public String getFirstName() {
            return firstName;
        }
    
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getLastName() {
            return lastName;
        }
    
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
    
        public String getPhoneNumber() {
            return phoneNumber;
        }
    
        public void setPhoneNumber(String phoneNumber) {
            this.phoneNumber = phoneNumber;
        }
    }

    Finally, the Data Access Object

    The DAO has the following four methods:

    • createPatron
    • retrieve
    • removePatron
    • update

    We’ll look at each of these, although none of this will be new if you’ve looked at the first tutorial.

    createPatron

    Given the information to create a patron, instantiate one and then persiste it. Note that this is written in a style that will natually fit into a Session Bean.

        public Patron createPatron(final String fname, final String lname,
                final String phoneNumber, final Address a) {
            final Patron p = new Patron(fname, lname, phoneNumber, a);
            getEm().persist(p);
            return p;
        }

    retrieve

    This uses the find method built into the entity manager. It returns null if not found. It first sees if the object is in the first-level cache. If not, it retrieves it from the database.

        public Patron retrieve(final Long id) {
            return getEm().find(Patron.class, id);
        }

    removePatron

    To remove an object we have to find it first. You do not provide a class and a key. So we first retrieve it (it might already be in the cache so this may not involve a database hit. We then issue the remove of the object.

        public void removePatron(final Long id) {
            final Patron p = retrieve(id);
            if(p != null) {
                getEm().remove(p);
            }
        }

    update

    Update uses the merge method to get its work done. Note that it returns what is returned from merge. Why? The provided patron could be detached (retrieve during another transaction or from a different instance of an entity manager. If this is the case, then the object will not be put into the cache (and therefore become managed). Instead, a new instance is created and the contents of the paramter is copied into the new, managed instance. That new managed instance is returned. If the provided patron is managed, then there’s actually not need to even call this method because any changes made to the patron will be reflected in the patron after the transaction is closed.

        public Patron update(final Patron p) {
            return getEm().merge(p);
        }

    The rest of the class

    Here are the imports, attributes and getters/setters.

    package session;
    
    import javax.persistence.EntityManager;
    
    import entity.Address;
    import entity.Patron;
    
    public class PatronDao {
        private EntityManager em;
    
        public void setEm(final EntityManager em) {
            this.em = em;
        }
    
        public EntityManager getEm() {
            return em;
        }
    }

    V1 Second Test Suite


    title: JPA_Tutorial_3_V1_Second_Test_Suite — We started with Patron. In round 2, we add the basic support for the Book. The book dao needs the same basic tests:

    • Creating a Book
    • Removing a Book
    • Updating a Bookadd

    Note that we did not also include retrieving a book. We use this functionality in all of the tests anyway so I do not include a specific test for that functionality. This might seem like we’re not isolating tests perfectly but then I’ve never seen or come up with a “perfect” solution to this issue and this seems adequate to me.

    We’ve already written a test very much like the above list if you consider PatronTest. We can extract quite a bit of common code out of our PatronTest and reuse it in our BookTest class. Take a look at this base class (note the embedded comments contain background information):

    BaseDbDaoTest.java

    package session;
    
    import javax.persistence.EntityManagerFactory;
    import javax.persistence.Persistence;
    
    import org.apache.log4j.BasicConfigurator;
    import org.apache.log4j.Level;
    import org.apache.log4j.Logger;
    import org.junit.After;
    import org.junit.Before;
    import org.junit.BeforeClass;
    
    /**
     * A base class for tests that handles logger initialization, entity manager
     * factory and entity manager creation, associating an entity manager with a
     * dao, starting and rolling back transactions.
     */
    public abstract class BaseDbDaoTest {
        private EntityManagerFactory emf;
    
        /**
         * Once before the tests start running for a given class, init the logger
         * with a basic configuration and set the default reporting layer to error
         * for all classes whose package starts with org.
         */
        @BeforeClass
        public static void initLogger() {
            // Produce minimal output.
            BasicConfigurator.configure();
    
            // Comment this line to see a lot of initialization
            // status logging.
            Logger.getLogger("org").setLevel(Level.ERROR);
        }
    
        /**
         * Derived class is responsible for instantiating the dao. This method gives
         * the hook necessary to this base class to init the dao with an entity
         * manger in a per-test setup method.
         * 
         * @return The dao to be used for a given test. The type specified is a base
         *         class from which all dao's inherit. The test derived class will
         *         override this method and change the return type to the type of
         *         dao it uses. This is called **covariance**. Java 5 allows
         *         covariant return types. I.e. BookDaoTest's version of getDao()
         *         will return BookDao while PatronDao's version of getDao() will
         *         return Patron.
         */
        public abstract BaseDao getDao();
    
        /**
         * Before each test method, look up the entity manager factory, get the dao
         * and set a newly-created entity manager and begin a transaction.
         */
        @Before
        public void initEmfAndEm() {
            emf = Persistence.createEntityManagerFactory("lis");
            getDao().setEm(emf.createEntityManager());
            getDao().getEm().getTransaction().begin();
        }
    
        /**
         * After each test method, roll back the transaction started in the
         * 
         * @Before method then close both the entity manager and entity manager
         *         factory.
         */
        @After
        public void closeEmAndEmf() {
            getDao().getEm().getTransaction().rollback();
            getDao().getEm().close();
            emf.close();
        }
    }

    Now let’s see how this impacts the creation of our new BookTest class. We’ll start with everything but the tests and then look at each test.

    Everything but the Tests

    Here is the test class minus all of the tests.

    package session;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertNotNull;
    import static org.junit.Assert.assertNull;
    
    import java.util.Calendar;
    
    import org.junit.Test;
    
    import entity.Author;
    import entity.Book;
    import entity.Name;
    
    public class BookDaoTest extends BaseDbDaoTest {
        private BookDao dao;
    
        /**
         * By overriding this method, I'm able to provide a dao to the base class,
         * which then installs a new entity manager per test method execution. Note
         * that my return type is not the same as the base class' version. I return
         * BookDao whereas the base class returns BaseDao. Normally an overridden
         * method must return the same type. However, it is OK for an overridden
         * method to return a different type so long as that different type is a
         * subclass of the type returned in the base class. This is called
         * covariance.
         * 
         * @see session.BaseDbDaoTest#getDao()
         */
    
        @Override
        public BookDao getDao() {
            if (dao == null) {
                dao = new BookDao();
            }
            return dao;
        }
    }

    Creating a Book

    We wrote this pretty much the same as in the Patron test. It might seem like we could get further reuse between tests and we could but at the cost of probably a bit too much indirection.

        @Test
        public void createABook() {
            final Book b = createABookImpl();
            final Book found = getDao().retrieve(b.getId());
            assertNotNull(found);
    
        }
    
        private Book createABookImpl() {
            final Author a1 = new Author(new Name("Bill", "Burke"));
            final Author a2 = new Author(new Name("Richard", "Monson-Haefel"));
            return getDao().create("Enterprise JavaBeans 3.0", "978-0-596-00978-6",
                    Calendar.getInstance().getTime(), a1, a2);
        }

    Removing a Book

    This test method looks just like one in the PatronTest class. If you’re looking for an advanced exercise, consider moving all of the tests in the base class and making the derived class methods use them somehow. Warning, you might want to look up annotation inheritance.

        @Test
        public void removeABook() {
            final Book b = createABookImpl();
            Book found = getDao().retrieve(b.getId());
            assertNotNull(found);
            getDao().remove(b.getId());
            found = getDao().retrieve(b.getId());
            assertNull(found);
        }

    Updating a Book

        @Test
        public void updateABook() {
            final Book b = createABookImpl();
            final int initialAuthorCount = b.getAuthors().size();
            b.addAuthor(new Author(new Name("New", "Author")));
            getDao().update(b);
            final Book found = getDao().retrieve(b.getId());
            assertEquals(initialAuthorCount + 1, found.getAuthors().size());
        }

    Try to find a non- existant book

        @Test
        public void tryToFindBookThatDoesNotExist() {
            final Book b = getDao().retrieve(-1123123123l);
            assertNull(b);
        }

    Note that with the introduction of the base class we’ll also need to make changes to PatronTest. Here’s the updated version of PatronTest taking the new base class into consideration.

    PatronDaoTest.java Updated

    package session;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertFalse;
    import static org.junit.Assert.assertNotNull;
    import static org.junit.Assert.assertNull;
    
    import org.junit.Test;
    
    import entity.Address;
    import entity.Patron;
    
    /**
     * This class has been updated to take advantage of BaseDbDaoTest. In reality, I
     * just pulled the common functionality of pre test initialization and post test
     * initialization to a base class since I'm going to use it across several test
     * cases.
     */
    public class PatronDaoTest extends BaseDbDaoTest {
        private static final String NEW_PN = "555-555-5555";
        private PatronDao dao;
    
        /**
         * @see session.BaseDbDaoTest#getDao()
         * @see session.BookDaoTest#getDao()
         */
        @Override
        public PatronDao getDao() {
            if (dao == null) {
                dao = new PatronDao();
            }
            return dao;
        }
    
        @Test
        public void createAPatron() {
            final Patron p = createAPatronImpl();
            final Patron found = getDao().retrieve(p.getId());
    
            assertNotNull(found);
        }
    
        /**
         * I need to create patrons in several tests so it is factored out here.
         * 
         * @return Newly created patron already inserted into the database under the
         *         current transaction
         */
    
        private Patron createAPatronImpl() {
            final Address a = new Address("5080 Spectrum Drive", "Suite 700 West",
                    "Addison", "TX", "75001");
            return getDao().createPatron("Brett", "Schuchert", "972-555-1212", a);
        }
    
        @Test
        public void removeAPatron() {
            final Patron p = createAPatronImpl();
    
            getDao().removePatron(p.getId());
            final Patron found = getDao().retrieve(p.getId());
    
            assertNull(found);
        }
    
        @Test
        public void updateAPatron() {
            final Patron p = createAPatronImpl();
    
            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());
        }
    
        @Test
        public void tryToFindPatronThatDoesNotExist() {
            final Long id = -18128129831298l;
            final Patron p = getDao().retrieve(id);
            assertNull(p);
        }
    } 

    The Dao Classes

    The BookDao looks a whole lot like the PatronDao:

    BookDao.java

    package session;
    
    import java.util.Date;
    
    import entity.Author;
    import entity.Book;
    
    /**
     * This class offers the basic create, read, update, delete functions required
     * for a book. As we implement more complex requirements, we'll be coming back
     * to this class to add additional queries.
     */
    public class BookDao extends BaseDao {
        public Book create(final String title, final String isbn,
                final Date publishDate, Author... authors) {
            final Book b = new Book(title, isbn, publishDate, authors);
            getEm().persist(b);
            return b;
        }
    
        public Book retrieve(final Long id) {
            return getEm().find(Book.class, id);
        }
    
        public void remove(Long id) {
            final Book b = retrieve(id);
            if (b != null) {
                getEm().remove(b);
            }
        }
    
        public void update(Book b) {
            getEm().merge(b);
        }
    }

    Note that this class depends on a simple base class, the BaseDao, which offers support for storing the Entity Manager attribute:

    BaseDao.java

    package session;
    
    import javax.persistence.EntityManager;
    
    /**
     * A simple base class for all dao's. It offers 2 features. First, it has the
     * entity manager attribute. Second, it makes it possible to have a common test
     * base class with the getDao() method to allow for automatic initialization.
     */
    public abstract class BaseDao {
        private EntityManager em;
    
        public void setEm(final EntityManager em) {
            this.em = em;
        }
    
        public EntityManager getEm() {
            return em;
        }
    }

    And finally, here’s the updated PatronDao that has been rewritten to use the BaseDao.

    PatronDao.java

    package session;
    
    import entity.Address;
    import entity.Patron;
    
    /**
     * This class supports basic create, read, update, delete functionality for the
     * Patron. As with Book, as we implement more requirements we'll be revisiting
     * this class to extend its functionality.
     */
    public class PatronDao extends BaseDao {
        public Patron createPatron(final String fname, final String lname,
                final String phoneNumber, final Address a) {
            final Patron p = new Patron(fname, lname, phoneNumber, a);
            getEm().persist(p);
            return p;
        }
    
        public Patron retrieve(final Long id) {
            return getEm().find(Patron.class, id);
        }
    
        public void removePatron(final Long id) {
            final Patron p = retrieve(id);
            if (p != null) {
                getEm().remove(p);
            }
        }
    
        public Patron update(final Patron p) {
            return getEm().merge(p);
        }
    }

    The Entity Model

    We’ve added support for a Book and along the way we had to add in a few more classes. After the second test suite, we’re up to the following entities:

    Entity Description
    Address This entity represents the address for both an Author and a Patron. In the first tutorial we embedded this class. Now we’re allowing it to exist in its own table as a first-class citizen rather than embedding it.
    Author Books and Authors have a bi-directional, many to many relationship with each other. That is, a book has one to many Authors and an Author has one to many books. This entity represents one author and maintains a Set representing each of its books. We treat the Author as the secondary part of the relationship and the book as Primary.
    Book The book is a key entity in our system. It maintains a set of Authors and is considered the master of the bi-directional relationship. In version 1 of our system, the relationship between Books and Patrons is direct. We’ll change that in version 2.
    Name Authors and Patrons both have a name. Rather than duplicate the definition of names in both classes, we create a Name entity. This entity is embeddable, meaning its attributes will be stored as columns in the entities in which it is contained rather than as rows in a table all of its own.
    Patron The patron borrows books, so it has a Set as well as an embedded Name.

    Now let’s review the code for each of these entities. As with previous examples, pay attention to the embedded comments.

    Address

    package entity;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    
    /**
     * This class will be known to JPA as an entity. It represents an address. It is
     * a fairly simple class that gets stored in its own table called ADDRESS. The
     * column names equal the names of the attributes.
     */
    
    @Entity
    public class Address {
        /**
         * The next attribute is a key column in the database with a
         * database-specific generated unique value.
         */
        @Id
        @GeneratedValue
        private Long id;
    
        /**
         * The next attribute will be stored in a column with space for 50
         * characters and cannot be null (the default)
         */
        @Column(length = 50)
        private String streetAddress1;
    
        /**
         * The next attribute will be stored in a column with space for 50
         * characters and it can be null(nullable = true).
         */
        @Column(length = 50, nullable = true)
        private String streetAddress2;
        @Column(length = 20)
        private String city;
        @Column(length = 2)
        private String state;
        @Column(length = 9)
        private String zip;
    
        public Address() {
        }
    
        public Address(final String sa1, final String sa2, final String city,
                final String state, final String zip) {
            setStreetAddress1(sa1);
            setStreetAddress2(sa2);
            setCity(city);
            setState(state);
            setZip(zip);
        }
    
        public String getCity() {
            return city;
        }
    
        public void setCity(final String city) {
            this.city = city;
        }
    
        public String getState() {
            return state;
        }
    
        public void setState(final String state) {
            this.state = state;
        }
    
        public String getStreetAddress1() {
            return streetAddress1;
        }
    
        public void setStreetAddress1(final String streetAddress1) {
            this.streetAddress1 = streetAddress1;
        }
    
        public String getStreetAddress2() {
            return streetAddress2;
        }
    
        public void setStreetAddress2(final String streetAddress2) {
            this.streetAddress2 = streetAddress2;
        }
    
        public String getZip() {
            return zip;
        }
    
        public void setZip(final String zip) {
            this.zip = zip;
        }
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    }

    Author

    package entity;
    
    import java.util.Set;
    
    import javax.persistence.Embedded;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.ManyToMany;
    
    /**
     * I am an entity with a bidirectional relationship to Book. The Book is
     * considered the master of the relationship.
     */
    @Entity
    public class Author {
        @Id
        @GeneratedValue
        private Long id;
    
        /**
         * The next attribute is embedded directly in me. That means its attributes
         * will be directly stored in columns in the same table as me rather than
         * being in its own table with key to itself and foreign key back to me.
         */
        @Embedded
        private Name name;
    
        /**
         * A book might be written by several authors and an author might write
         * several books. Therefore we maintain a many-to-many relationship between
         * books authors. It's bidirectional as well.
         */
        @ManyToMany
        private Set<Book> booksWritten;
    
        public Author(final Name name) {
            setName(name);
        }
    
        public Author() {
        }
    
        public Set<Book> getBooksWritten() {
            return booksWritten;
        }
    
        public void addBook(final Book b) {
            booksWritten.add(b);
            b.addAuthor(this);
        }
    
        public void setBooksWritten(final Set<Book> booksWritten) {
            this.booksWritten = booksWritten;
        }
    
        public Long getId() {
            return id;
        }
    
        public void setId(final Long id) {
            this.id = id;
        }
    
        /**
         * We are storing Authors in sets so we need to define some definition of
         * equality. We've decided to use Name as that definition. You might think
         * to use the id field for equality but it may not be assigned before this
         * object is placed in a collection so we have to use a more natural
         * definition of equality.
         */
    
        @Override
        public boolean equals(final Object object) {
            if (object instanceof Author) {
                final Author rhs = (Author) object;
                return getName().equals(rhs.getName());
            }
            return false;
        }
    
        /**
         * The hash code should relate to the equals method. And as mentioned there,
         * we cannot use the id field for the hash code because it is likely we
         * won't have an id already assigned by the database before we try put this
         * object in a collection that requires the hashCode method (such as HashSet
         * or HashMap). So we use a natural part of the object for its
         * interpretation of hash code.
         */
    
        @Override
        public int hashCode() {
            return getName().hashCode();
        }
    
        public Name getName() {
            return name;
        }
    
        public void setName(final Name name) {
            this.name = name;
        }
    }

    Book

    package entity;
    
    import java.util.Calendar;
    import java.util.Date;
    import java.util.HashSet;
    import java.util.Set;
    
    import javax.persistence.CascadeType;
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.ManyToMany;
    import javax.persistence.ManyToOne;
    import javax.persistence.NamedQueries;
    import javax.persistence.NamedQuery;
    
    /**
     * I represent a Book. I have one named query to find a book by its isbn number.
     * I also have a many to many relationship with author. Since I define the
     * mappedBy, I'm the (arbitrarily picked) master of the relationship. I also
     * take care of cascading changes to the database.
     */
    
    @Entity
    public class Book {
        @Id
        @GeneratedValue
        private Long id;
        @Column(length = 100, nullable = false)
        private String title;
        @Column(length = 20, nullable = false)
        private String isbn;
        private Date printDate;
    
        /**
         * Authors may have written several books and vice-versa. We had to pick one
         * side of this relationship as the primary one and we picked books. It was
         * arbitrary but since we're dealing with books, we decided to make this
         * side the primary size. The mappedBy connects this relationship to the one
         * that is in Author. When we merge or persist, changes to this collection
         * and the contents of the collection will be updated. That is, if we update
         * the name of the author in the set, when we persist the book, the author
         * will also get updated.
         * 
         * Note that if we did not have the cascade setting here, they if we tried
         * to persist a book with an unmanaged author (e.g. a newly created one),
         * the entity manager would contain of a transient object.
         */
        @ManyToMany(mappedBy = "booksWritten", cascade = { CascadeType.PERSIST,
                CascadeType.MERGE })
        private Set<Author> authors;
    
        /**
         * I may be borrowed. If so, then I'll know who that is. In this version, I
         * simply have a direct relationship with the Patron. In the next version,
         * we'll create a table to capture the details of borrowing a resource.
         */
        @ManyToOne
        private Patron borrowedBy;
    
        public Book(final String t, final String i, final Date printDate,
                final Author... authors) {
            setTitle(t);
            setIsbn(i);
            setPrintDate(printDate);
            for (Author a : authors) {
                addAuthor(a);
            }
        }
    
        public Book() {
        }
    
        public Set<Author> getAuthors() {
            if (authors == null) {
                authors = new HashSet<Author>();
            }
            return authors;
        }
    
        public void setAuthors(final Set<Author> authors) {
            this.authors = authors;
        }
    
        public Long getId() {
            return id;
        }
    
        public void setId(final Long id) {
            this.id = id;
        }
    
        public String getIsbn() {
            return isbn;
        }
    
        public void setIsbn(final String isbn) {
            this.isbn = isbn;
        }
    
        public Date getPrintDate() {
            return printDate;
        }
    
        public void setPrintDate(final Date printDate) {
            this.printDate = printDate;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(final String title) {
            this.title = title;
        }
    
        public void addAuthor(final Author author) {
            getAuthors().add(author);
        }
    
        @Override
        public boolean equals(final Object rhs) {
            return rhs instanceof Book && ((Book) rhs).getIsbn().equals(getIsbn());
        }
    
        @Override
        public int hashCode() {
            return getIsbn().hashCode();
        }
    
        public boolean wasWrittenBy(Author a) {
            return getAuthors().contains(a);
        }
    
        public boolean checkedOutBy(Patron p) {
            return p != null && p.equals(getBorrowedBy());
        }
    
        public Date calculateDueDateFrom(Date checkoutDate) {
            final Calendar c = Calendar.getInstance();
            c.setTime(checkoutDate);
            c.add(Calendar.DATE, 14);
            return c.getTime();
        }
    
        public Patron getBorrowedBy() {
            return borrowedBy;
        }
    
        public void setBorrowedBy(Patron borrowedBy) {
            this.borrowedBy = borrowedBy;
        }
    }

    Name

    package entity;
    
    import javax.persistence.Column;
    import javax.persistence.Embeddable;
    
    /**
     * Rather than repeat first name/last name in both Patron and Author, we create
     * an embedded class. The fields of this class end up as columns in the table
     * that contains the class that embeds this entity. That is, both author and
     * patron will have a firstName and lastName column.
     */
    
    @Embeddable
    public class Name {
        @Column(length = 20, nullable = false)
        private String firstName;
        @Column(length = 30, nullable = false)
        private String lastName;
    
        public Name() {
    
        }
    
        public Name(final String firstName, final String lastName) {
            setFirstName(firstName);
            setLastName(lastName);
        }
    
        public String getFirstName() {
            return firstName;
        }
    
        public void setFirstName(String firstName) {
            if (firstName != null) {
                this.firstName = firstName;
            } else {
                this.firstName = "";
            }
        }
    
        public String getLastName() {
            return lastName;
        }
    
        public void setLastName(String lastName) {
            if (lastName != null) {
                this.lastName = lastName;
            } else {
                this.lastName = "";
            }
        }
    }

    Patron

    package entity;
    
    import java.util.HashSet;
    import java.util.Set;
    
    import javax.persistence.CascadeType;
    import javax.persistence.Column;
    import javax.persistence.Embedded;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.OneToMany;
    import javax.persistence.OneToOne;
    
    @Entity
    public class Patron {
        @Id
        @GeneratedValue
        private Long id;
    
        @Embedded
        private Name name;
    
        @Column(length = 11, nullable = false)
        private String phoneNumber;
    
        /**
         * This next field refers to an object that is stored in another table. All
         * updates are cascaded. So if you persist me, my address, which is in
         * another table, will be persisted automatically. Updates and removes are
         * also cascaded automatically.
         * 
         * Note that cascading removes is a bit dangerous. In this case I know that
         * the address is owned by only one Patron. In general you need to be
         * careful automatically removing objects in related tables due to possible
         * constraint violations.
         */
        @OneToOne(cascade = CascadeType.ALL)
        private Address address;
    
        /**
         * A Patron may checkout several books. This collection
         */
    
        @OneToMany(mappedBy = "borrowedBy", cascade = { CascadeType.MERGE,
                CascadeType.PERSIST })
        private Set<Book> borrowedBooks;
    
        public Patron(final String fName, final String lName, final String phone,
                final Address a) {
            setName(new Name(fName, lName));
            setPhoneNumber(phone);
            setAddress(a);
        }
    
        public Address getAddress() {
            return address;
        }
    
        public void setAddress(Address address) {
            this.address = address;
        }
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getPhoneNumber() {
            return phoneNumber;
        }
    
        public void setPhoneNumber(String phoneNumber) {
            this.phoneNumber = phoneNumber;
        }
    
        public Set<Book> getBorrowedBooks() {
            if (borrowedBooks == null) {
                borrowedBooks = new HashSet<Book>();
            }
            return borrowedBooks;
        }
    
        public void setBorrowedBooks(Set<Book> borrowedBooks) {
            this.borrowedBooks = borrowedBooks;
        }
    
        public void addBook(final Book b) {
            getBorrowedBooks().add(b);
        }
    
        public void removeBook(final Book b) {
            getBorrowedBooks().remove(b);
        }
    
        public Name getName() {
            return name;
        }
    
        public void setName(Name name) {
            this.name = name;
        }
    }

    V1 Third Test Suite


    title: JPA_Tutorial_3_V1_Third_Test_Suite — Typical enterprise systems are build on a multi-tiered system. There are usually at least three tiers:

    • Presentation
    • Business
    • Integration

    There might be a few more, but for now this list of tiers will suite us well.

    Our first two tests produced Data Access Objects (dao)’s. These two dao’s hide the details of getting books and patrons. They fall under the integration layer.

    Now it is time to add a higher-level concept, the Library. The Library class represents a Facade to the underlying system. This so-called facade will be the primary interface to the middle tier of our system.

    Of course, along the way we’ll end up doing yet more refactoring to accommodate this new suite of tests.

    Library

    First we’ll start with a new suite of tests for this Library facade. For this first pass, we’ll write several basic tests and a few tests that move us closer to use-case like functionality.

    Adding a Book

        @Test
        public void addBook() {
            final Book b = createBook();
            Set<Author> authors = b.getAuthors();
            final Book found = library.findBookById(b.getId());
    
            assertTrue(found.getAuthors().containsAll(authors));
        }
    
        private Book createBook() {
            final Author a1 = new Author(new Name("Christian", "Bauer"));
            final Author a2 = new Author(new Name("Gavin", "King"));
    
            return library.createBook("Hibernate In Action", ISBN, Calendar
                    .getInstance().getTime(), a1, a2);
        }

    Lookup a Book that Does Not Exist

    Notice that this test has different results than the same test in the BookDaoTest. In this case we expect an exception to be thrown while in the case of the BookDaoTest we just get back null. Why? The dao has no way of knowing what the policy should be regarding not finding objects, whereas the Library facade can set the policy.

        @Test(expected = EntityNotFoundException.class)
        public void lookupBookThatDoesNotExist() {
            library.findBookById(ID_DNE);
        }

    Adding a Patron

        @Test
        public void addPatron() {
            final Patron p = createPatron();
            final Patron found = library.findPatronById(p.getId());
            assertNotNull(found);
        }
    
        private Patron createPatron() {
            final Address a = new Address("5080 Spectrum Drive", "", "Dallas",
                    "TX", "75001");
            return library.createPatron(PATRON_ID, "Brett", "Schuchert",
                    "555-1212", a);
        }

    Lookup a Patron that Does Not Exist

    As with the BookDao, the PatronDao simply returns null if an object is not found by ID. The Library changes that null result into an exception.

        @Test(expected = EntityNotFoundException.class)
        public void lookupPatronThatdoesNotExist() {
            library.findPatronById(ID_DNE);
        }

    Checking out a book to a patron

        @Test
        public void checkoutBook() {
            final Book b = createBook();
            final Patron p = createPatron();
            library.checkout(p.getId(), b.getId());
    
            final Book foundBook = library.findBookById(b.getId());
            final Patron foundPatron = library.findPatronById(p.getId());
    
            assertTrue(foundBook.isOnLoanTo(foundPatron));
            assertTrue(foundPatron.isBorrowing(foundBook));
        }

    Returning a book

        @Test
        public void returnBook() {
            final Book b = createBook();
            final Patron p = createPatron();
            library.checkout(p.getId(), b.getId());
    
            final int booksBefore = p.getBorrowedBooks().size();
            assertTrue(b.isOnLoan());
            library.returnBook(b.getId());
            assertEquals(booksBefore - 1, p.getBorrowedBooks().size());
            assertFalse(b.isOnLoan());
        }

    Returning a book that is not checked out

        @Test
        public void returnBookThatsNotCheckedOut() {
            final Book b = createBook();
            assertFalse(b.isOnLoan());
            library.returnBook(b.getId());
            assertFalse(b.isOnLoan());
        }

    Checking out a Book that is Already Checked Out

        @Test(expected = BookAlreadyCheckedOut.class)
        public void checkoutBookThatIsAlreadyCheckedOut() {
            final Book b = createBook();
            final Patron p1 = createPatron();
            final Patron p2 = createPatron();
    
            library.checkout(p1.getId(), b.getId());
            library.checkout(p2.getId(), b.getId());
        }

    Checkout a Book that Does Not Exist

        @Test(expected = EntityNotFoundException.class)
        public void checkoutBookThatDoesNotExist() {
            final Patron p = createPatron();
            library.checkout(p.getId(), ID_DNE);
        }

    Checkout a Book to a Patron that Does Not Exist

        @Test(expected = EntityNotFoundException.class)
        public void checkoutBookToPatronThatDoesNotExist() {
            final Book b = createBook();
            library.checkout(ID_DNE, b.getId());
        }

    LibraryTest.java

    Here’s the shell of the test.

    package session;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertFalse;
    import static org.junit.Assert.assertNotNull;
    import static org.junit.Assert.assertTrue;
    
    import java.util.Calendar;
    import java.util.Set;
    
    import javax.persistence.EntityNotFoundException;
    
    import org.junit.Before;
    import org.junit.Test;
    
    import entity.Address;
    import entity.Author;
    import entity.Book;
    import entity.Name;
    import entity.Patron;
    import exception.BookAlreadyCheckedOut;
    
    public class LibraryTest extends EntityManagerBasedTest {
        private static final long ID_DNE = -443123222l;
        private static final String PATRON_ID = "113322";
        private static final String ISBN = "1-932394-15-X";
        private Library library;
    
        @Before
        public void setupLibrary() {
            final BookDao bd = new BookDao();
            bd.setEm(getEm());
            final PatronDao pd = new PatronDao();
            pd.setEm(getEm());
            library = new Library();
            library.setBookDao(bd);
            library.setPatronDao(pd);
        }
    }

    EntityManagerBasedTest

    This new class inherits from a new base class called EnttyManagerBasedTest. This class factors out just the part of initialization related to the entity manager and the transactions from the BaseDbDaoTest.

    package session;
    
    import javax.persistence.EntityManager;
    import javax.persistence.EntityManagerFactory;
    import javax.persistence.Persistence;
    
    import org.apache.log4j.BasicConfigurator;
    import org.apache.log4j.Level;
    import org.apache.log4j.Logger;
    import org.junit.After;
    import org.junit.Before;
    import org.junit.BeforeClass;
    
    /**
     * Our tests use an entity manager. The first pass at the BaseDbDaoTest forced
     * initialization of a Dao. That works for the dao-based tests but not all
     * tests. This class factors out just the part that sets up and cleans up the
     * entity manager.
     * 
     */
    public abstract class EntityManagerBasedTest {
        private EntityManagerFactory emf;
        private EntityManager em;
    
        /**
         * Once before the tests start running for a given class, init the logger
         * with a basic configuration and set the default reporting layer to error
         * for all classes whose package starts with org.
         */
        @BeforeClass
        public static void initLogger() {
            // Produce minimal output.
            BasicConfigurator.configure();
    
            // Comment this line to see a lot of initialization
            // status logging.
            Logger.getLogger("org").setLevel(Level.ERROR);
        }
    
        /**
         * Before each test method, look up the entity manager factory, then create
         * the entity manager.
         */
        @Before
        public void initEmfAndEm() {
            emf = Persistence.createEntityManagerFactory("lis");
            em = emf.createEntityManager();
            em.getTransaction().begin();
        }
    
        /**
         * After each test method, roll back the transaction started in the -at-
         * Before method then close both the entity manager and entity manager
         * factory.
         */
        @After
        public void closeEmAndEmf() {
            getEm().getTransaction().rollback();
            getEm().close();
            emf.close();
        }
    
        public EntityManager getEm() {
            return em;
        }
    
        public void setEm(EntityManager em) {
            this.em = em;
        }
    }

    BaseDbDaoTest

    Here is yet another updated BaseDbDaoTest that reflects the new base class.

    package session;
    
    import org.junit.Before;
    
    /**
     * A base class for tests that handles logger initialization, entity manager
     * factory and entity manager creation, associating an entity manager with a
     * dao, starting and rolling back transactions.
     */
    public abstract class BaseDbDaoTest extends EntityManagerBasedTest {
        /**
         * Derived class is responsible for instantiating the dao. This method gives
         * the hook necessary to this base class to init the dao with an entity
         * manger in a per-test setup method.
         * 
         * @return The dao to be used for a given test. The type specified is a base
         *         class from which all dao's inherit. The test derived class will
         *         override this method and change the return type to the type of
         *         dao it uses. This is called **covariance**. Java 5 allows
         *         covariant return types. I.e. BookDaoTest's version of getDao()
         *         will return BookDao while PatronDao's version of getDao() will
         *         return Patron.
         */
        public abstract BaseDao getDao();
    
        /**
         * The -at- before method in the base class executes first. After that, init
         * the dao with the entity manager.
         */
        @Before
        public void initDao() {
            getDao().setEm(getEm());
        }
    }

    The Exception

    We’ve added one new unchecked exception to our system, BookAlreadyCheckedOut. Here it is:

    package exception;
    
    /**
     * A simple unchecked exception reflecting a particular business rule violation.
     * A book cannot be checked out if it is already checked out.
     * 
     * This exception inherits from RuntimeException (or it is an unchecked
     * exception). Why? The policy of whether to use checked or unchecked exceptions
     * is project dependent. We are using this for learning about EJB3 and JPA and
     * NOT about how to write exceptions, so using one policy versus the other is
     * arbitrary for our purposes. Working with unchecked exceptions is a bit looser
     * but also keeps the code looking a bit cleaner, so we've gone with unchecked
     * exceptions.
     */
    public class BookAlreadyCheckedOut extends RuntimeException {
        private static final long serialVersionUID = 2286908621531520488L;
    
        final Long bookId;
    
        public BookAlreadyCheckedOut(final Long bookId) {
            this.bookId = bookId;
        }
    
        public Long getBookId() {
            return bookId;
        }
    }

    Library

    This class is all new.

    package session;
    
    import java.util.Date;
    import java.util.List;
    
    import javax.persistence.EntityNotFoundException;
    
    import entity.Address;
    import entity.Author;
    import entity.Book;
    import entity.Patron;
    import exception.BookAlreadyCheckedOut;
    
    public class Library {
        private BookDao bookDao;
        private PatronDao patronDao;
    
        public BookDao getBookDao() {
            return bookDao;
        }
    
        public void setBookDao(BookDao bookDao) {
            this.bookDao = bookDao;
        }
    
        public PatronDao getPatronDao() {
            return patronDao;
        }
    
        public void setPatronDao(PatronDao patronDao) {
            this.patronDao = patronDao;
        }
    
        public Book createBook(final String title, final String isbn,
                final Date date, final Author a1, final Author a2) {
            return getBookDao().create(title, isbn, date, a1, a2);
        }
    
        public List<Book> findBookByIsbn(String isbn) {
            return getBookDao().findByIsbn(isbn);
        }
    
        public Patron createPatron(final String patronId, final String fname,
                final String lname, final String phoneNumber, final Address a) {
            return getPatronDao().createPatron(fname, lname, phoneNumber, a);
        }
    
        public Patron findPatronById(final Long id) {
            final Patron p =  getPatronDao().retrieve(id);
           if(p == null) {
               throw new EntityNotFoundException(
                   String.format("Patron with id: %d does not exist", id));
           }
            return p;
        }
    
        public void checkout(final Long patronId, final Long bookId) {
            final Book b = findBookById(bookId);
            if(b.isOnLoan()) {
                throw new BookAlreadyCheckedOut(bookId);
            }
            
            final Patron p = findPatronById(patronId);
    
            p.addBook(b);
            b.setBorrowedBy(p);
    
            getPatronDao().update(p);
        }
    
        public Book findBookById(Long id) {
            final Book b = getBookDao().findById(id);
            if(b == null) {
                throw new EntityNotFoundException(
                    String.format("Book with Id:%d does not exist", id));
            }
            return b;
        }
    
        public void returnBook(Long id) {
            final Book b = getBookDao().findById(id);
    
            if (b.isOnLoan()) {
                final Patron p = b.checkin();
                p.removeBook(b);
                getPatronDao().update(p);
            }
        }
    }

    BookDao

    The tests use the findByIsbn() method, which returns a collection of Books. Why does findByIsbn() return a collection of books? The isbn is not unique; the book id is the only unique column. If we enforced a unique isbn, then there could only be one book of a given isbn in the library.

    We’ve also added a method, findById, which should return a unique value (or null).

        @SuppressWarnings("unchecked")
        public List<Book> findByIsbn(String isbn) {
            return getEm().createNamedQuery("Book.findByIsbn").setParameter("isbn",
                    isbn).getResultList();
        }
    
        public Book findById(Long id) {
            return getEm().find(Book.class, id);
        }

    Util

    We need a basic utility to assist with equality. This utility will handle when we have null references.

    EqualsUtil

    package util;
    
    /**
     * We typically need to compare two object and also perform null checking. This
     * class provides a simple wrapper to accomplish doing so.
     */
    
    public class EqualsUtil {
        private EqualsUtil() {
            // I'm a utility class, do not instantiate me
        }
    
        public static boolean equals(final Object lhs, final Object rhs) {
            return lhs == null && rhs == null
                    || (lhs != null && rhs != null && lhs.equals(rhs));
    
        }
    }

    EqualsUtilTest

    package util;
    
    import static org.junit.Assert.assertFalse;
    import static org.junit.Assert.assertTrue;
    
    import org.junit.Test;
    
    public class EqualsUtilTest {
        private static final String BRETT = "Brett";
    
        @Test
        public void bothNull() {
            assertTrue(EqualsUtil.equals(null, null));
        }
    
        @Test
        public void bothNonNullAndEqual() {
            assertTrue(EqualsUtil.equals(BRETT, BRETT));
        }
    
        @Test
        public void bothNonNullAndNotEquals() {
            assertFalse(EqualsUtil.equals(BRETT, BRETT.toLowerCase()));
        }
    
        @Test
        public void lhsNullRhsNonNull() {
            assertFalse(EqualsUtil.equals(null, BRETT));
        }
    
        @Test
        public void lhsNonNullRhsNull() {
            assertFalse(EqualsUtil.equals(BRETT, null));
        }
    }

    Entity Changes

    Book

    The book is somewhat changed. First it needs to import util.EqualsUtil (as shown below). It also contains some named queries and three new methods: isOnLoanTo, isOnLoan and checkin. The code below shows these changes.

    import util.EqualsUtil;
    
    /**
     * I represent a Book. I have one named query to find a book by its isbn number.
     * I also have a many to many relationship with author. Since I define the
     * mappedBy, I'm the (arbitrarily picked) master of the relationship. I also
     * take care of cascading changes to the database.
     */
    
    @Entity
    /**
     * A named query must have a globally unique name. That is why these are named
     * "Book."... These queries could be associated with any entity. Given that they
     * clearly deal with books, it seems appropriate to put them here. These will
     * probably be pre-compiled and in any case available from the entity manager by
     * using em.getNamedQuery("Book.findById").
     */
    @NamedQueries( {
            @NamedQuery(name = "Book.findById", 
                        query = "SELECT b FROM Book b where b.id = :id"),
            @NamedQuery(name = "Book.findByIsbn", 
                        query = "SELECT b FROM Book b WHERE b.isbn = :isbn") })
    public class Book {
        public boolean isOnLoanTo(final Patron foundPatron) {
            return EqualsUtil.equals(getBorrowedBy(), foundPatron);
        }
    
        public boolean isOnLoan() {
            return getBorrowedBy() != null;
        }
    
        public Patron checkin() {
            final Patron p = getBorrowedBy();
            setBorrowedBy(null);
            return p;
        }
    }

    Patron

    There was only one change to Patron. We want to be able to ask the Patron if it is in fact borrowing a particular book.

        public boolean isBorrowing(final Book foundBook) {
            return getBorrowedBooks().contains(foundBook);
        }

    V1 Assignments


    title: JPA_Tutorial_3_V1_Assignments —

    Questions

    • Given our current solution, how would you add support for:
      • Return dates
      • Fines
    • Review how we return a book. Does it seem strange that we retrieve the book, get the patron, update both of them and then merge the patron?
    • How are the first two questions related?

    Advanced: If you have spare time

    Write a simple user interface. Your interface should support the following operations:

    • Add a book
    • Add a patron
    • Checkout a book to a patron
    • Return a book

    Your choice on the type of user interface, text, servlet, swing, …

    V2 Requirements: Relational Table


    title: JPA_Tutorial_3_V2_Requirements — In the first version of our simple system, we had a 1 to many relationship from Patron to Book and a many to 1 relationship from Book to Patron. On a UML diagram, this looks like the following (only showing this one relationship):

    In reality, this relationship might better be described as:

    If you take the UML interpretation of this diagram, it says that when a Patron and Book come together, there are attributes associated withe the relationship between them.

    We could simply put a few dates on the book, checkoutDate and dueDate. The problem is that when the book is checked out we have three non-null attributes: checkoutDate, dueDate, borrowedBy, and when the book is not checked out, those same three attributes are null. This seems a bit awkward.

    So we are going to update our example to provide support for tracking the date a book was checked out and the date it is due. We will also calculate fines and move a bit closer to being able to fully support the use case to checkout books.

    V2 Updated Library Test Suite


    title: JPA_Tutorial_3_V2_First_Test_Suite — In this second version, we add the following features:

    • We track when a book was checked out and when it is due
    • We calculate fines when returning books
    • We associate fines with patrons
    • We allow the patron to pay for their fines
    • We disallow patrons from checking out books when they have fines

    Along the way, we make a lot of additions and changes. Based on the updated LibraryTest, here is a list of all the changes I made to get things to work (note if you choose to start from the test and make things work yourself, you results may vary):

    src/entity

    Book Now has an optional Loan object instead of a direct reference to a Patron.
    Fine New class, represents an individual fine generated from returning one book late. A Patron has zero to many of these.
    Loan New class, represents the information related to the relationship between Patron and Book. A Patron has a One to Many relationship with Loan while a book as a One to One that is optional (can be null).
    LoanId A key-class for the Loan class. The key is two columns, a foreign key to Patron and a foreign key to Book.
    Patron Now has a One to Many relationship with both Loan and Fines. It also has several new methods in support of those new/changed attributes.

    src/exception

    BookNotCheckedOut New exception class. Thrown when trying to return a book that is not checked out.
    InsufficientFunds New exception class. Thrown when Patron tries to pay fines but does not tender enough cash.
    PatronHasFines New exception class. Thrown when Patron tries to check out a book but already has fines.

    src/session

    Library Substantially changed in support of the new requirements.
    LoanDao New class. Provides some simple query support directly related to loan class.

    src/util

    DateTimeUtil A new class. Provides some basic date/time utilities.

    test/session

    LibraryTest Several new tests in support of new functionality.

    test/util

    DateTimeUtilTest Several test in support of new utility class.

    New Utility

    To calculate fines, we needed to determine the number of days late a Patron returned a Book. Here are the tests for that class:

    DateTimeUtilTest.java

    package util;
    
    import static org.junit.Assert.assertEquals;
    
    import java.util.Calendar;
    import java.util.Date;
    
    import org.junit.Test;
    
    /**
     * A class to test the DateTimeUtil class. Verifies that the calculation for the
     * number of days between to dates is correct for several different scenarios.
     */
    public class DateTimeUtilTest {
        public static final Date DATE = Calendar.getInstance().getTime();
    
        @Test
        public void dateBetween0() {
            assertEquals(0, DateTimeUtil.daysBetween(DATE, DATE));
        }
    
        @Test
        public void dateBetween1() {
            assertEquals(1, DateTimeUtil.daysBetween(DATE, addDaysToDate(DATE, 1)));
        }
    
        @Test
        public void dateBetweenMinus1() {
            assertEquals(-1, DateTimeUtil
                    .daysBetween(DATE, addDaysToDate(DATE, -1)));
        }
    
        @Test
        public void startInDstEndOutOfDst() {
            final Date inDst = createDate(2006, 9, 1);
            final Date outDst = createDate(2006, 10, 1);
    
            assertEquals(31, DateTimeUtil.daysBetween(inDst, outDst));
        }
    
        @Test
        public void startOutDstEndInDst() {
            final Date inDst = createDate(2006, 9, 1);
            final Date outDst = createDate(2006, 10, 1);
    
            assertEquals(-31, DateTimeUtil.daysBetween(outDst, inDst));
        }
    
        @Test
        public void overLeapDayNoChangeInDst() {
            final Date beforeLeapDay = createDate(2004, 1, 27);
            final Date afterLeapDay = createDate(2004, 2, 1);
    
            assertEquals(3, DateTimeUtil.daysBetween(beforeLeapDay, afterLeapDay));
        }
    
        @Test
        public void overLeapDayAndOverDstChange() {
            final Date beforeLeapDayNonDst = createDate(2004, 1, 27);
            final Date afterLeapDayAndDst = createDate(2004, 3, 5);
    
            assertEquals(38, DateTimeUtil.daysBetween(beforeLeapDayNonDst,
                    afterLeapDayAndDst));
        }
    
        private Date addDaysToDate(final Date date, final int days) {
            Calendar c = Calendar.getInstance();
            c.setTime(date);
            c.add(Calendar.DAY_OF_YEAR, days);
            return c.getTime();
        }
    
        private Date createDate(final int year, final int month, final int day) {
            final Calendar c = Calendar.getInstance();
            c.set(Calendar.YEAR, year);
            c.set(Calendar.MONTH, month);
            c.set(Calendar.DAY_OF_MONTH, day);
    
            return c.getTime();
        }
    }

    DateTimeUtil

    package util;
    
    import java.util.Calendar;
    import java.util.Date;
    import java.util.GregorianCalendar;
    
    /**
     * This is a simple class containing date/time utilities to avoid proliferation
     * of duplicate code through the system.
     */
    public class DateTimeUtil {
        private static final int MS_IN_HOUR = 1000 * 60 * 60;
        private static final int MS_IN_Day = 24 * MS_IN_HOUR;
    
        /**
         * This is a class with all static methods (often called a utility class).
         * To document the fact that it should be used without first being
         * instantiated, we make the constructor private. Furthermore, some code
         * evaluation tools, such as PMD, will complain about an empty method body,
         * so we add a comment in the method body to appease such tools.
         * 
         */
        private DateTimeUtil() {
            // I'm a utility class, do not instantiate me
        }
    
        /**
         * Remove all of the time elements from a date.
         */
        public static void removeTimeFrom(final Calendar c) {
            c.clear(Calendar.AM_PM);
            c.clear(Calendar.HOUR_OF_DAY);
            c.clear(Calendar.HOUR);
            c.clear(Calendar.MINUTE);
            c.clear(Calendar.SECOND);
            c.clear(Calendar.MILLISECOND);
        }
    
        /**
         * This is a simple algorithm to calculate the number of days between two
         * dates. It is not very accurate, does not take into consideration leap
         * years, etc. Do not use this in production code. It serves our purposes
         * here.
         * 
         * @param d1
         *            "from date"
         * @param d2
         *            "to date"
         * 
         * @return number of times "midnight" is crossed between these two dates,
         *         logically this is d2 - d1.
         */
        public static int daysBetween(final Date d1, final Date d2) {
            GregorianCalendar c1 = new GregorianCalendar();
            c1.setTime(d1);
            GregorianCalendar c2 = new GregorianCalendar();
            c2.setTime(d2);
    
            final long t1 = c1.getTimeInMillis();
            final long t2 = c2.getTimeInMillis();
            long diff = t2 - t1;
    
            final boolean startInDst = c1.getTimeZone().inDaylightTime(d1);
            final boolean endInDst = c2.getTimeZone().inDaylightTime(d2);
    
            if (startInDst && !endInDst) {
                diff -= MS_IN_HOUR;
            }
            if (!startInDst && endInDst) {
                diff += MS_IN_HOUR;
            }
    
            return (int) (diff / MS_IN_Day);
        }
    }

    The Exceptions

    Here are the three new exception classes:

    BookNotCheckedOut

    package exception;
    
    /**
     * A simple unchecked exception reflecting a particular business rule violation.
     * A book cannot be checked out if it is already checked out.
     * 
     * This exception inherits from RuntimeException (or it is an unchecked
     * exception). Why? The policy of whether to use checked or unchecked exceptions
     * is project dependent. We are using this for learning about EJB3 and JPA and
     * NOT about how to write exceptions, so using one policy versus the other is
     * arbitrary for our purposes. Working with unchecked exceptions is a bit looser
     * but also keeps the code looking a bit cleaner, so we've gone with unchecked
     * exceptions.
     */
    public class BookNotCheckedOut extends RuntimeException {
        private static final long serialVersionUID = 2286908621531520488L;
    
        final Long bookId;
    
        public BookNotCheckedOut(final Long bookId) {
            this.bookId = bookId;
        }
    
        public Long getBookId() {
            return bookId;
        }
    }

    InsufficientFunds.java

    package exception;
    
    /**
     * Thrown when a Patron attempts to pay less that then total fines owed.
     */
    public class InsufficientFunds extends RuntimeException {
        private static final long serialVersionUID = -735261730912439200L;
    }

    PatronHasFines.java

    package exception;
    
    /**
     * Thrown when Patron attempts to checkout a book but has fines.
     */
    public class PatronHasFines extends RuntimeException {
        private static final long serialVersionUID = 2868510410691634148L;
    
        double totalFines;
    
        public PatronHasFines(final double amount) {
            this.totalFines = amount;
        }
    
        public double getTotalFines() {
            return totalFines;
        }
    }

    The Library Test

    Many of the original tests are different from the previous version. Additionally, there are many new tests. Here is the test. Once you get this in to your system, you might want to simply get all of the tests methods to compile and then get the tests to pass.

    Doing so is approaching formal TDD. It is different in a few important respects:

    • You are given the tests rather than writing them yourself
    • You are working on many tests at once rather than just one (or a very few) at a time

    Even so, this suite of test fully express the new set of requirements for version 2.

    package session;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertFalse;
    import static org.junit.Assert.assertNotNull;
    import static org.junit.Assert.assertTrue;
    import static org.junit.Assert.fail;
    
    import java.util.Calendar;
    import java.util.Date;
    import java.util.List;
    import java.util.Set;
    
    import javax.persistence.EntityNotFoundException;
    
    import org.junit.Before;
    import org.junit.BeforeClass;
    import org.junit.Test;
    
    import util.DateTimeUtil;
    import entity.Address;
    import entity.Author;
    import entity.Book;
    import entity.Name;
    import entity.Patron;
    import exception.BookAlreadyCheckedOut;
    import exception.BookNotCheckedOut;
    import exception.InsufficientFunds;
    import exception.PatronHasFines;
    
    public class LibraryTest extends EntityManagerBasedTest {
        private static final long ID_DNE = -443123222l;
        private static final String PATRON_ID = "113322";
        private static final String ISBN = "1-932394-15-X";
        private static Date CURRENT_DATE;
        private static Date CURRENT_PLUS_8;
        private static Date CURRENT_PLUS_14;
        private static Date CURRENT_PLUS_15;
        private Library library;
    
        @Before
        public void setupLibrary() {
            final BookDao bd = new BookDao();
            bd.setEm(getEm());
            final PatronDao pd = new PatronDao();
            pd.setEm(getEm());
            final LoanDao ld = new LoanDao();
            ld.setEm(getEm());
            library = new Library();
            library.setBookDao(bd);
            library.setPatronDao(pd);
            library.setLoanDao(ld);
        }
    
        @BeforeClass
        public static void setupDates() {
            Calendar c = Calendar.getInstance();
            DateTimeUtil.removeTimeFrom(c);
            CURRENT_DATE = c.getTime();
            c.add(Calendar.DAY_OF_MONTH, 8);
            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();
        }
    
        @Test
        public void addBook() {
            final Book b = createBook();
            Set<Author> authors = b.getAuthors();
            final Book found = library.findBookById(b.getId());
    
            assertTrue(found.getAuthors().containsAll(authors));
        }
    
        @Test(expected = EntityNotFoundException.class)
        public void lookupBookThatDoesNotExist() {
            library.findBookById(ID_DNE);
        }
    
        @Test
        public void addPatron() {
            final Patron p = createPatron();
            final Patron found = library.findPatronById(p.getId());
            assertNotNull(found);
        }
    
        @Test(expected = EntityNotFoundException.class)
        public void lookupPatronThatDoesNotExist() {
            library.findPatronById(ID_DNE);
        }
    
        @Test
        public void checkoutBook() {
            final Book b1 = createBook();
            final Book b2 = createBook();
            final Patron p = createPatron();
            library.checkout(p.getId(), CURRENT_DATE, b1.getId(), b2.getId());
    
            final List<Book> list = library.listBooksOnLoanTo(p.getId());
    
            assertEquals(2, list.size());
    
            for (Book b : list) {
                assertTrue(b.isOnLoanTo(p));
                assertTrue(b.dueDateEquals(CURRENT_PLUS_14));
            }
        }
    
        @Test
        public void returnBook() {
            final Book b = createBook();
            final Patron p = createPatron();
            library.checkout(p.getId(), CURRENT_DATE, b.getId());
    
            final int booksBefore = p.getCheckedOutResources().size();
            assertTrue(b.isCheckedOut());
            library.returnBook(CURRENT_PLUS_8, b.getId());
            assertEquals(booksBefore - 1, p.getCheckedOutResources().size());
            assertFalse(b.isCheckedOut());
            assertEquals(0, p.getFines().size());
        }
    
        @Test
        public void returnBookLate() {
            final Book b = createBook();
            final Patron p = createPatron();
    
            library.checkout(p.getId(), CURRENT_DATE, b.getId());
            library.returnBook(CURRENT_PLUS_15, b.getId());
    
            assertEquals(1, p.getFines().size());
            assertEquals(.25, p.calculateTotalFines());
        }
    
        @Test(expected = BookNotCheckedOut.class)
        public void returnBookThatsNotCheckedOut() {
            final Book b = createBook();
            assertFalse(b.isCheckedOut());
            library.returnBook(CURRENT_PLUS_8, b.getId());
        }
    
        @Test(expected = BookAlreadyCheckedOut.class)
        public void checkoutBookThatIsAlreadyCheckedOut() {
            final Book b = createBook();
            final Patron p1 = createPatron();
            final Patron p2 = createPatron();
    
            library.checkout(p1.getId(), CURRENT_DATE, b.getId());
            library.checkout(p2.getId(), CURRENT_DATE, b.getId());
        }
    
        @Test(expected = EntityNotFoundException.class)
        public void checkoutBookThatDoesNotExist() {
            final Patron p = createPatron();
            library.checkout(p.getId(), CURRENT_DATE, ID_DNE);
        }
    
        @Test(expected = EntityNotFoundException.class)
        public void checkoutBookToPatronThatDoesNotExist() {
            final Book b = createBook();
            library.checkout(ID_DNE, CURRENT_DATE, b.getId());
        }
    
        @Test
        public void findOverdueBooks() {
            final Patron p = createPatron();
            final Book b1 = createBook();
            final Book b2 = createBook();
            library.checkout(p.getId(), CURRENT_DATE, b1.getId());
            library.checkout(p.getId(), CURRENT_PLUS_8, b2.getId());
            final List<Book> notOverdue = library
                    .findAllOverdueBooks(CURRENT_PLUS_8);
            assertEquals(0, notOverdue.size());
            final List<Book> overdue = library.findAllOverdueBooks(CURRENT_PLUS_15);
            assertEquals(1, overdue.size());
            assertTrue(overdue.contains(b1));
        }
    
        @Test
        public void patronsWithOverdueBooks() {
            final Patron p = createPatron();
            final Book b1 = createBook();
            final Book b2 = createBook();
            library.checkout(p.getId(), CURRENT_DATE, b1.getId());
            library.checkout(p.getId(), CURRENT_PLUS_8, b2.getId());
            final List<Patron> noPatrons = library
                    .findAllPatronsWithOverdueBooks(CURRENT_PLUS_14);
            assertEquals(0, noPatrons.size());
            final List<Patron> onePatron = library
                    .findAllPatronsWithOverdueBooks(CURRENT_PLUS_15);
            assertEquals(1, onePatron.size());
        }
    
        @Test
        public void calculateTotalFinesForPatron() {
            final Patron p = createPatron();
            final Book b1 = createBook();
            final Book b2 = createBook();
            library.checkout(p.getId(), CURRENT_DATE, b1.getId());
            library.checkout(p.getId(), CURRENT_DATE, b2.getId());
            library.returnBook(CURRENT_PLUS_15, b1.getId(), b2.getId());
            assertEquals(.5, library.calculateTotalFinesFor(p.getId()));
        }
    
        @Test
        public void payFineExactAmount() {
            final Patron p = createPatron();
            final Book b1 = createBook();
            library.checkout(p.getId(), CURRENT_DATE, b1.getId());
            library.returnBook(CURRENT_PLUS_15, b1.getId());
            double change = library.tenderFine(p.getId(), .25);
            assertEquals(0d, change);
            assertEquals(0, p.getFines().size());
        }
    
        @Test(expected = InsufficientFunds.class)
        public void payFineInsufficientFunds() {
            final Patron p = createPatron();
            final Book b1 = createBook();
            library.checkout(p.getId(), CURRENT_DATE, b1.getId());
            library.returnBook(CURRENT_PLUS_15, b1.getId());
            library.tenderFine(p.getId(), .20);
        }
    
        /**
         * This is an example of a test where we expect an exception. However,
         * unlike other tests where we use expected=ExceptionClass.class, we need to
         * catch the exception because we are additionally verifying a value in the
         * thrown exception. This test is written how you'd write a test expecting
         * an exception prior to JUnit 4.
         */
        @Test
        public void patronCannotCheckoutWithFines() {
            final Patron p = createPatron();
            final Book b1 = createBook();
            library.checkout(p.getId(), CURRENT_DATE, b1.getId());
            library.returnBook(CURRENT_PLUS_15, b1.getId());
    
            final Book b2 = createBook();
    
            try {
                library.checkout(p.getId(), CURRENT_DATE, b2.getId());
                fail(String.format("Should have thrown exception: %s",
                        PatronHasFines.class.getName()));
            } catch (PatronHasFines e) {
                assertEquals(.25, e.getTotalFines());
            }
        }
    
        private Book createBook() {
            final Author a1 = new Author(new Name("Christian", "Bauer"));
            final Author a2 = new Author(new Name("Gavin", "King"));
    
            return library.createBook("Hibernate In Action", ISBN, Calendar
                    .getInstance().getTime(), a1, a2);
        }
    
        private Patron createPatron() {
            final Address a = new Address("5080 Spectrum Drive", "", "Dallas",
                    "TX", "75001");
            return library.createPatron(PATRON_ID, "Brett", "Schuchert",
                    "555-1212", a);
        }
    }

    V2 Updated Sessions


    title: JPA_Tutorial_3_V2_Updated_Sessions — We’ve added a new session, LoanDao. It essentially provides a few queries dealing with loans such as finding all overdue books or finding all patrons with overdue books.

    LoanDao.java

    package session;
    
    import java.util.Date;
    import java.util.List;
    
    import javax.persistence.NoResultException;
    
    import entity.Book;
    import entity.Loan;
    import entity.Patron;
    
    /**
     * Provide some basic queries focused around the Loan object. These queries
     * could have been placed in either the PatronDao or BookDao. However, neither
     * seemed like quite the right place so we created this new Dao.
     */
    public class LoanDao extends BaseDao {
    
        /**
         * Given a book id, find the associated loan or return null if none found.
         * 
         * @param bookId
         *            Id of book on loan
         * 
         * @return Loan object that holds onto bookId
         */
        public Loan getLoanFor(Long bookId) {
            try {
                return (Loan) getEm().createNamedQuery("Loan.byBookId")
                        .setParameter("bookId", bookId).getSingleResult();
            } catch (NoResultException e) {
                return null;
            }
        }
    
        public void remove(final Loan l) {
            getEm().remove(l);
        }
    
        /**
         * Return books that are due after the compareDate.
         * 
         * @param compareDate
         *            If a book's due date is after compareDate, then it is included
         *            in the list. Note that this named query uses projection. Have
         *            a look at Loan.java.
         * 
         * @return a list of all the books that were due after this date.
         */
        @SuppressWarnings("unchecked")
        public List<Book> listAllOverdueBooks(final Date compareDate) {
            return getEm().createNamedQuery("Loan.overdueBooks").setParameter(
                    "date", compareDate).getResultList();
        }
    
        /**
         * Essentially the same query as listAllOverdueBooks but we return the
         * Patrons instead of the books. This method uses a named query that uses
         * projection.
         * 
         * @param compareDate
         *            If a patron has at least one book that was due after the
         *            compare date, include them.
         * 
         * @return A list of the patrons with at least one overdue book
         */
        @SuppressWarnings("unchecked")
        public List<Patron> listAllPatronsWithOverdueBooks(final Date compareDate) {
            return getEm().createNamedQuery("Loan.patronsWithOverdueBooks")
                    .setParameter("date", compareDate).getResultList();
        }
    
        /**
         * Return all books on loan to the provided patron id.
         * 
         * @param patronId
         *            If patron id is invalid, this method will not notice it.
         * 
         * @return Zero or more books on loan to the patron in question
         */
        @SuppressWarnings("unchecked")
        public List<Book> listBooksOnLoanTo(final Long patronId) {
            return getEm().createNamedQuery("Loan.booksLoanedTo").setParameter(
                    "patronId", patronId).getResultList();
        }
    }

    Library.java

    package session;
    
    import java.util.Date;
    import java.util.List;
    
    import javax.persistence.EntityNotFoundException;
    
    import entity.Address;
    import entity.Author;
    import entity.Book;
    import entity.Loan;
    import entity.Patron;
    import exception.BookAlreadyCheckedOut;
    import exception.BookNotCheckedOut;
    import exception.PatronHasFines;
    
    /**
     * This class provides a basic facade to the library system. If we had a user
     * interface, it would interact with this object rather than dealing with all of
     * the underlying Daos.
     */
    public class Library {
        private BookDao bookDao;
        private PatronDao patronDao;
        private LoanDao loanDao;
    
        public BookDao getBookDao() {
            return bookDao;
        }
    
        public void setBookDao(final BookDao bookDao) {
            this.bookDao = bookDao;
        }
    
        public PatronDao getPatronDao() {
            return patronDao;
        }
    
        public void setPatronDao(final PatronDao patronDao) {
            this.patronDao = patronDao;
        }
    
        public LoanDao getLoanDao() {
            return loanDao;
        }
    
        public void setLoanDao(final LoanDao loanDao) {
            this.loanDao = loanDao;
        }
    
        public Book createBook(final String title, final String isbn,
                final Date date, final Author a1, final Author a2) {
            return getBookDao().create(title, isbn, date, a1, a2);
        }
    
        public List<Book> findBookByIsbn(String isbn) {
            return getBookDao().findByIsbn(isbn);
        }
    
        public Patron createPatron(final String patronId, final String fname,
                final String lname, final String phoneNumber, final Address a) {
            return getPatronDao().createPatron(fname, lname, phoneNumber, a);
        }
    
        public Patron findPatronById(final Long id) {
            final Patron p = getPatronDao().retrieve(id);
            if (p == null) {
                throw new EntityNotFoundException(String.format(
                        "Patron with id: %d does not exist", id));
            }
            return p;
        }
    
        public Book findBookById(Long id) {
            final Book b = getBookDao().findById(id);
            if (b == null) {
                throw new EntityNotFoundException(String.format(
                        "Book with Id:%d does not exist", id));
            }
            return b;
        }
    
        public void returnBook(final Date checkinDate, final Long... bookIds) {
            for (Long bookId : bookIds) {
                final Loan l = getLoanDao().getLoanFor(bookId);
    
                if (l == null) {
                    throw new BookNotCheckedOut(bookId);
                }
    
                l.checkin(checkinDate);
    
                getLoanDao().remove(l);
            }
        }
    
        public void checkout(final Long patronId, final Date checkoutDate,
                final Long... bookIds) {
            final Patron p = findPatronById(patronId);
    
            double totalFines = p.calculateTotalFines();
    
            if (totalFines > 0.0d) {
                throw new PatronHasFines(totalFines);
            }
    
            for (Long id : bookIds) {
                final Book b = findBookById(id);
    
                if (b.isCheckedOut()) {
                    throw new BookAlreadyCheckedOut(id);
                }
    
                p.checkout(b, checkoutDate);
            }
        }
    
        public List<Book> listBooksOnLoanTo(final Long patronId) {
            return getLoanDao().listBooksOnLoanTo(patronId);
        }
    
        public List<Book> findAllOverdueBooks(final Date compareDate) {
            return getLoanDao().listAllOverdueBooks(compareDate);
        }
    
        public List<Patron> findAllPatronsWithOverdueBooks(final Date compareDate) {
            return getLoanDao().listAllPatronsWithOverdueBooks(compareDate);
        }
    
        public double calculateTotalFinesFor(final Long patronId) {
            return getPatronDao().retrieve(patronId).calculateTotalFines();
        }
    
        public double tenderFine(final Long patronId, double amountTendered) {
            final Patron p = getPatronDao().retrieve(patronId);
            return p.pay(amountTendered);
        }
    }

    V2 Updated Entities


    title: JPA_Tutorial_3_V2_Updated_Entities — The biggest change this version was the addition of a Loan entity. A loan represents information about the relationship between Book and Patron. It specifically stores the checkout date and the due date. The Loan entity represents a so-called join table in the database. There are ways to specify a join table without creating an Entity, however we created the entity because we wanted to store additional information about the relationship. It also, arguably, makes some of our queries easier having Loan as an entity rather that just described as a join table.

    Loan.java

    package entity;
    
    import java.util.Date;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.Id;
    import javax.persistence.IdClass;
    import javax.persistence.JoinColumn;
    import javax.persistence.ManyToOne;
    import javax.persistence.NamedQueries;
    import javax.persistence.NamedQuery;
    import javax.persistence.Temporal;
    import javax.persistence.TemporalType;
    
    /**
     * I'm an entity with a 2-part key. The first part of my key is a Book id, the
     * second is a Patron id.
     * <P>
     * To make this work, we must do several things: Use the annotation IdClass,
     * Specify each of the parts of the id by using Id annotation (2 times in this
     * case), Set each Id column to both insertable = false and updatable = false,
     * Create an attribute representing the type of the id (Book, Patron), Use
     * JoinColumn with the name = id and insertable = false, updatable = false.
     */
    
    @Entity
    @IdClass(LoanId.class)
    @NamedQueries( {
            @NamedQuery(name = "Loan.booksLoanedTo", 
                query = "SELECT l.book FROM Loan l WHERE l.patron.id = :patronId"),
            @NamedQuery(name = "Loan.byBookId", 
                query = "SELECT l FROM Loan l WHERE l.bookId = :bookId"),
            @NamedQuery(name = "Loan.overdueBooks", 
                query = "SELECT l.book FROM Loan l WHERE l.dueDate < :date"),
            @NamedQuery(name = "Loan.patronsWithOverdueBooks", 
                query = "SELECT l.patron FROM Loan l WHERE l.dueDate < :date") })
    public class Loan {
        /**
         * Part 1 of a 2-part Key. The name must match the name in LoanId.
         */
        @Id
        @Column(name = "bookId", insertable = false, updatable = false)
        private Long bookId;
    
        /**
         * Part 2 of a 2-part Key. The name must match the name in LoanId.
         */
        @Id
        @Column(name = "patronId", insertable = false, updatable = false)
        private Long patronId;
    
        /**
         * A duplicate column in a sense, this one gives us the actual Patron rather
         * than just having the id of the Patron.
         * 
         * In the reference material I read, putting in insertable and updatable 
         * false did not seem required. However, when using the hibernate entity
         * manger I got a null pointer exception and had to step through the source
         * code to fix the problem.
         */
        @ManyToOne
        @JoinColumn(name = "patronId", insertable = false, updatable = false)
        private Patron patron;
    
        /**
         * Same comment as for patron attribute above plus...
         *
         * It seems this should be a OneToOne relationship but doing so will give
         * you an obscure exception with little explanation as to why.
         */
        @ManyToOne
        @JoinColumn(name = "bookId", insertable = false, updatable = false)
        private Book book;
    
        /**
         * The date type can represent a date, a time or a time stamp (date and
         * time). In our case we just want the date.
         */
        @Temporal(TemporalType.DATE)
        @Column(updatable = false)
        private Date checkoutDate;
    
        @Temporal(TemporalType.DATE)
        @Column(updatable = false)
        private Date dueDate;
    
        public Loan() {
        }
    
        public Loan(final Book b, final Patron p, final Date checkoutDate) {
            setBookId(b.getId());
            setPatronId(p.getId());
            setBook(b);
            setPatron(p);
            setCheckoutDate(checkoutDate);
            setDueDate(b.calculateDueDateFrom(checkoutDate));
        }
    
        public Book getBook() {
            return book;
        }
    
        public void setBook(final Book book) {
            this.book = book;
        }
    
        public Long getBookId() {
            return bookId;
        }
    
        public void setBookId(final Long bookId) {
            this.bookId = bookId;
        }
    
        public Date getCheckoutDate() {
            return checkoutDate;
        }
    
        public void setCheckoutDate(final Date checkoutDate) {
            this.checkoutDate = checkoutDate;
        }
    
        public Date getDueDate() {
            return dueDate;
        }
    
        public void setDueDate(final Date dueDate) {
            this.dueDate = dueDate;
        }
    
        public Patron getPatron() {
            return patron;
        }
    
        public void setPatron(final Patron patron) {
            this.patron = patron;
        }
    
        public Long getPatronId() {
            return patronId;
        }
    
        public void setPatronId(final Long patronId) {
            this.patronId = patronId;
        }
    
        public void checkin(final Date checkinDate) {
            getBook().checkin(checkinDate);
            getPatron().checkin(this);
        }
    }

    LoanId.java

    package entity;
    
    import java.io.Serializable;
    
    /**
     * I'm a custom, multi-part key. I represent the key of a Loan object, which
     * consists of a Patron id and a Book id.
     * 
     * These two values together must be unique. The names of my attributes are the
     * same as the names used in Loan (patronId, bookId).
     * 
     * I also must be Serializable.
     */
    public class LoanId implements Serializable {
        private static final long serialVersionUID = -6272344103273093529L;
    
        /**
         * The following two fields must have names that match the names used in the
         * Loan class.
         */
    
        private Long patronId;
        private Long bookId;
    
        public LoanId() {
        }
    
        public LoanId(final Long patronId, final Long bookId) {
            this.patronId = patronId;
            this.bookId = bookId;
        }
    
        public Long getBookId() {
            return bookId;
        }
    
        public Long getPatronId() {
            return patronId;
        }
    
        @Override
        public boolean equals(final Object rhs) {
            return rhs instanceof LoanId && ((LoanId) rhs).bookId.equals(bookId)
                    && ((LoanId) rhs).patronId.equals(patronId);
        }
    
        @Override
        public int hashCode() {
            return patronId.hashCode() * bookId.hashCode();
        }
    }

    Book.java

    package entity;
    
    import java.util.Calendar;
    import java.util.Date;
    import java.util.HashSet;
    import java.util.Set;
    
    import javax.persistence.CascadeType;
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.ManyToMany;
    import javax.persistence.NamedQueries;
    import javax.persistence.NamedQuery;
    import javax.persistence.OneToOne;
    
    import util.DateTimeUtil;
    
    /**
     * I represent a Book. I have one named query to find a book by its isbn number.
     * I have a many to many relationship with author. Since I define the mappedBy,
     * I'm the (arbitrarily picked) master of the relationship. I also take care of
     * cascading changes to the database. I also have One To One relationship with a
     * Loan that is optional (so it can be null). If I have a loan, I'm checked out
     * and the loan knows the Patron, checkout date and due date.
     */
    
    @Entity
    /**
     * A named query must have a globally unique name. That is why these are named
     * "Book."... These queries could be associated with any entity. Given that this
     * query deals with books, it seems appropriate to put it here. Named queries
     * will probably be pre-compiled. They are available from the entity manager by
     * using em.getNamedQueyr("Book.findById").
     */
    @NamedQueries( 
            { @NamedQuery(name = "Book.findByIsbn", 
                query = "SELECT b FROM Book b WHERE b.isbn = :isbn") })
    public class Book {
        @Id
        @GeneratedValue
        private Long id;
        @Column(length = 100, nullable = false)
        private String title;
        @Column(length = 20, nullable = false)
        private String isbn;
        private Date printDate;
    
        /**
         * Authors may have written several books and vice-versa. We had to pick one
         * side of this relationship as the primary one and we picked books. It was
         * arbitrary but since we're dealing with books, we decided to make this
         * side the primary size. The mappedBy connects this relationship to the one
         * that is in Author. When we merge or persist, changes to this collection
         * and the contents of the collection will be updated. That is, if we update
         * the name of the author in the set, when we persist the book, the author
         * will also get updated.
         * 
         * Note that if we did not have the cascade setting here, they if we tried
         * to persist a book with an unmanaged author (e.g. a newly created one),
         * the entity manager would contain of a transient object.
         */
        @ManyToMany(mappedBy = "booksWritten", cascade = { CascadeType.PERSIST,
                CascadeType.MERGE })
        private Set<Author> authors;
    
        /**
         * Now, instead of directly knowing the patron who has borrowed me as in the
         * previous version, I now hold on to a Loan object. The loan tracks both
         * me, the patron as well as the checkout and due dates.
         */
        @OneToOne(mappedBy = "book", cascade = CascadeType.PERSIST, optional = true)
        private Loan loan;
    
        public Book(final String t, final String i, final Date printDate,
                final Author... authors) {
            setTitle(t);
            setIsbn(i);
            setPrintDate(printDate);
            for (Author a : authors) {
                addAuthor(a);
            }
        }
    
        public Book() {
        }
    
        public Set<Author> getAuthors() {
            if (authors == null) {
                authors = new HashSet<Author>();
            }
            return authors;
        }
    
        public void setAuthors(final Set<Author> authors) {
            this.authors = authors;
        }
    
        public Long getId() {
            return id;
        }
    
        public void setId(final Long id) {
            this.id = id;
        }
    
        public String getIsbn() {
            return isbn;
        }
    
        public void setIsbn(final String isbn) {
            this.isbn = isbn;
        }
    
        public Date getPrintDate() {
            return printDate;
        }
    
        public void setPrintDate(final Date printDate) {
            this.printDate = printDate;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(final String title) {
            this.title = title;
        }
    
        public void addAuthor(final Author author) {
            getAuthors().add(author);
        }
    
        public Loan getLoan() {
            return loan;
        }
    
        public void setLoan(Loan loan) {
            this.loan = loan;
        }
    
        @Override
        public boolean equals(final Object rhs) {
            return rhs instanceof Book && ((Book) rhs).getIsbn().equals(getIsbn());
        }
    
        @Override
        public int hashCode() {
            return getIsbn().hashCode();
        }
    
        public boolean wasWrittenBy(Author a) {
            return getAuthors().contains(a);
        }
    
        public Date calculateDueDateFrom(Date checkoutDate) {
            final Calendar c = Calendar.getInstance();
            c.setTime(checkoutDate);
            c.add(Calendar.DATE, 14);
            return c.getTime();
        }
    
        public boolean isOnLoanTo(final Patron p) {
            return (getLoan() == null && p == null) || getLoan() != null
                    && getLoan().getPatron().equals(p);
        }
    
        public boolean isCheckedOut() {
            return getLoan() != null;
        }
    
        public boolean dueDateEquals(final Date date) {
            return (date == null && !isCheckedOut())
                    || getLoan().getDueDate().equals(date);
        }
    
        public Date getDueDate() {
            if (isCheckedOut()) {
                return getLoan().getDueDate();
            }
            return null;
        }
    
        public void checkin(Date checkinDate) {
            final Date dueDate = getDueDate();
    
            if (getLoan().getDueDate().before(checkinDate)) {
                final double amount = .25 * DateTimeUtil.daysBetween(dueDate,
                        checkinDate);
                final Fine f = new Fine(amount, checkinDate, this);
                getLoan().getPatron().addFine(f);
            }
    
            setLoan(null);
        }
    
    }

    Fine.java

    package entity;
    
    import java.util.Date;
    
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.OneToOne;
    import javax.persistence.Temporal;
    import javax.persistence.TemporalType;
    
    /**
     * I represent a single fine assigned to a Patron for a book returned after its
     * due date.
     * 
     * I use new new features of JPA. 
     */
    @Entity
    public class Fine {
        @Id
        @GeneratedValue
        private Long id;
        private double amount;
    
        @Temporal(TemporalType.DATE)
        private Date dateAdded;
    
        @OneToOne
        private Book book;
    
        public Fine() {
        }
    
        public Fine(final double amount, final Date dateAdded, final Book book) {
            setAmount(amount);
            setDateAdded(dateAdded);
            setBook(book);
        }
    
        public Book getBook() {
            return book;
        }
    
        public void setBook(Book book) {
            this.book = book;
        }
    
        public Date getDateAdded() {
            return dateAdded;
        }
    
        public void setDateAdded(Date dateAdded) {
            this.dateAdded = dateAdded;
        }
    
        public double getAmount() {
            return amount;
        }
    
        public void setAmount(double fine) {
            this.amount = fine;
        }
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
    }

    Patron.java

    package entity;
    
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    
    import javax.persistence.CascadeType;
    import javax.persistence.Column;
    import javax.persistence.Embedded;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.OneToMany;
    import javax.persistence.OneToOne;
    import javax.persistence.OrderBy;
    
    import exception.InsufficientFunds;
    
    @Entity
    public class Patron {
        @Id
        @GeneratedValue
        private Long id;
    
        @Embedded
        private Name name;
    
        @Column(length = 11, nullable = false)
        private String phoneNumber;
    
        /**
         * This next field refers to an object that is stored in another table. All
         * updates are cascaded. So if you persist me, my address, which is in
         * another table, will be persisted automatically. Updates and removes are
         * also cascaded automatically.
         * 
         * Note that cascading removes is a bit dangerous. In this case I know that
         * the address is owned by only one Patron. In general you need to be
         * careful automatically removing objects in related tables due to possible
         * constraint violations.
         */
        @OneToOne(cascade = CascadeType.ALL)
        private Address address;
    
        /**
         * A Patron may have several books on loan.
         */
        @OneToMany(mappedBy = "patron", cascade = { CascadeType.PERSIST,
                CascadeType.MERGE })
        private List<Loan> checkedOutResources;
    
        /**
         * I have zero to many fines. The fines are ordered by the date they were
         * added to me.
         */
        @OneToMany(cascade = CascadeType.ALL)
        @OrderBy("dateAdded")
        private List<Fine> fines;
    
        public Patron(final String fName, final String lName, final String phone,
                final Address a) {
            setName(new Name(fName, lName));
            setPhoneNumber(phone);
            setAddress(a);
        }
    
        public Address getAddress() {
            return address;
        }
    
        public void setAddress(Address address) {
            this.address = address;
        }
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getPhoneNumber() {
            return phoneNumber;
        }
    
        public void setPhoneNumber(String phoneNumber) {
            this.phoneNumber = phoneNumber;
        }
    
        public List<Loan> getCheckedOutResources() {
            if (checkedOutResources == null) {
                checkedOutResources = new ArrayList<Loan>();
            }
    
            return checkedOutResources;
        }
    
        public void setCheckedOutResources(List<Loan> checkedOutResources) {
            this.checkedOutResources = checkedOutResources;
        }
    
        public Name getName() {
            return name;
        }
    
        public void setName(Name name) {
            this.name = name;
        }
    
        public void removeLoan(final Loan loan) {
            getCheckedOutResources().remove(loan);
        }
    
        public void addLoan(Loan l) {
            getCheckedOutResources().add(l);
        }
    
        public List<Fine> getFines() {
            if (fines == null) {
                fines = new ArrayList<Fine>();
            }
            return fines;
        }
    
        public void setFines(List<Fine> fines) {
            this.fines = fines;
        }
    
        public void checkout(final Book b, final Date checkoutDate) {
            final Loan l = new Loan(b, this, checkoutDate);
            getCheckedOutResources().add(l);
            b.setLoan(l);
        }
    
        public void addFine(final Fine f) {
            getFines().add(f);
        }
    
        public void checkin(final Loan loan) {
            getCheckedOutResources().remove(loan);
        }
    
        public double calculateTotalFines() {
            double sum = 0;
            for (Fine f : getFines()) {
                sum += f.getAmount();
            }
    
            return sum;
        }
    
        /**
         * I clear fines depending on amount tendered. Note that the cascade mode is
         * set to all, so if I delete records from my set, they will be removed from
         * the database.
         * 
         * @param amountTendered
         * 
         * @return balance after the payment
         */
    
        public double pay(final double amountTendered) {
            double totalFines = calculateTotalFines();
            if (totalFines <= amountTendered) {
                setFines(new ArrayList<Fine>());
                return amountTendered - totalFines;
            } else {
                throw new InsufficientFunds();
            }
        }
    }

    V2 Assignments


    title: JPA_Tutorial_3_V2_Assignments —

    Patrons with Overdue Books

    A Patron cannot checkout a book if they have any outstanding fines. Can they checkout a book if they have overdue books?

    Write a test to determine if they can or cannot. If they can, update the system somehow to make it impossible to checkout books if the Patron has overdue books.

    Paying Fines

    Right now, a Patron can only pay fines if they tender at least as much as their total fines. Allow a Patron to pay partial fines. Paying from the oldest to the newest fine, remove as many complete fines. Once they no longer have enough money to pay a fine fully, calculate the balance due and leave all the remaining fines associated with a Patron.

    Write the following tests to support your new functionality:

    • Pay all of a Patron’s fines in 2 steps with exact change.
    • Pay all of a Patron’s fines in 2 steps with more than the required amount.
    • Pay part of a Patron’s fines and then verify that they still cannot checkout books. Then pay the rest of the fines and verify that they can checkout books.

    Updated UI/Add a UI

    Consider adding a user interface (or updating the one you’ve already created). Support the new functionality of paying fines, listing fines, listing overdue books, listing patrons with overdue books, etc.

    Your user interface will need to handle the exceptions that might be thrown by the system (or you could just make sure to not do anything wrong).

    Advanced: Inheritance

    What happens if you create a new kind of resource, say a DVD. Now a patron can checkout a book or a DVD. Provide support for this functionality.

    Note: this will take some time and we’ll be looking at this kind of functionality later, so only attempt this assignment if you have a good amount of time.

    FAQ


    title: JPA_Tutorial_3_FAQ — JPA Tutorial 3 - FAQ

    • Phone number length is set to 11 in the annotation in the exercise, but phone numbers are 12. What’s happening?It will get truncated (in hypersonic)//
    • TDD: Is there a tool (eclipse plugin) to generate a concrete class based on a test case?Use Quick-fix (Ctrl-1 or click the icon on the sidebar) to create the class, and stub methods.//
    • When do Named Queries get compiled?When you create the EntityManager factory.//
    • Is there something that can use to see the db structure based on the mappings?After the entitymanager creates the db, use something like DB Visualizer//

    What did we learn/observe so far (mid-day)?

    • varargs for methods
    • cascading types - persist, all, refresh. propagates operations (merge/update/persist/refresh) across entities. Saves you from having to persist things.
    • jpa requires jdk 1.5
    • the @column annotation sets the name of the column in the database. also lets you specify things like width/length (attribute of the annotation).
    • debugging good
    • eclipse can gen hashcode/equals

    What did we learn/observe so far (end of day)?

    • Brett: Why we use a DAO .
    • Brett: Contrast the Library to the DAO’s .
    • ? When you get your objects, they’re fully configured (nice). Brett: makes it more testable
    • ? Defines attributes for a particular field in DB. Brett: affects physical characteristics as opposed to logical.
    • ? saves lines of code
    • ? brett: hide a warning based on actual query parameter type (not available at compile-time)
    • ? You can have only one version of the annotation on a given object
    • ? confusing, brett: heavy-handed/overkill
    • ? helps if you decide you want to change how you’re accessing your database.
    • ? have to look at code to understand what’s changing (head nods)
    • ) gives compiler ability to flag problem. some class discussion of when needed, when not…
    • l Brett:time/datastamp

    //What else have you learned today?//

    • e
    • )
    • .
    • ? defines the sort order

    //Day 3 - Tutorial #3 Questions//

    • Patron has a one-to-many for fines, but no many-to-one on the fines side. Why? Just an example of a uni-directional relationship.
    • Why is insertable false? (in exercise) We think: So that loan has to point at things which already exist. (as opposed to creating new loan/patron at same time)
    • ? Hold off on that because the next few exercises build up additional requirements which may change your mind

    //What else have you learned today?//

    • //How we can create/persist a given entity//
    • s
    • //Query syntax is better/easier//

    //Notes for Brett://

    • .

    s

    • A Patron can return a book while having an overdue book

      Below are the tests to check everything:

      And the new feature that doesn’t allow to checkout with overdue books.

    These tests are in LibraryTest.java

        @Test
        public void patronHasNumberOfOverdueBooks() {
            final Patron p = createPatron();
            final Book b1 = createBook();
            final Book b2 = createBook();
            library.checkout(p.getId(), CURRENT_DATE, b1.getId());
            int numOfBooks;
            numOfBooks = library.getNumberOfOverdueBooksOfPatron(p, CURRENT_PLUS_14);
            assertEquals(0, numOfBooks);
            library.checkout(p.getId(), CURRENT_PLUS_8, b2.getId());
            numOfBooks = library.getNumberOfOverdueBooksOfPatron(p, CURRENT_PLUS_15);
            assertEquals(1, numOfBooks);
            library.returnBook(CURRENT_PLUS_8, b1.getId());
            numOfBooks = library.getNumberOfOverdueBooksOfPatron(p, CURRENT_PLUS_15);
            assertEquals(0, numOfBooks);
        }
    
        // My change
        @Test
        public void patronCannotCheckoutWithOverdueBooks() {
            final Patron p = createPatron();
            final Book b1 = createBook();
            library.checkout(p.getId(), CURRENT_DATE, b1.getId());
    
            final Book b2 = createBook();
            try {
                library.checkout(p.getId(), CURRENT_PLUS_15, b2.getId());
                fail(String.format("1. Should have thrown exception: %s", PatronHasOverdueBooks.class
                        .getName()));
            }
            catch (PatronHasOverdueBooks e) {
                assertEquals(1, e.getNumberOfOverdueBooks());
                final Book b3 = createBook();
                library.checkout(p.getId(), CURRENT_DATE, b2.getId());
                try {
                    library.checkout(p.getId(), CURRENT_PLUS_15, b3.getId());
                    fail(String.format("2. Should have thrown exception: %s",
                            PatronHasOverdueBooks.class.getName()));
                }
                catch (PatronHasOverdueBooks e1) {
                    assertEquals(2, e1.getNumberOfOverdueBooks());
                }
                int numOfBooks = library.getNumberOfOverdueBooksOfPatron(p, CURRENT_PLUS_14);
                assertEquals(0, numOfBooks);
            }
        }

    And here are the changes in project:

    There is a new exception class called: PatronHasOverdueBooks Here it is:

    package exception;
    
    /**
     * Thrown when a Patron attempts checkout while he has overdue books.
     */
    public class PatronHasOverdueBooks extends RuntimeException {
        private static final long serialVersionUID = 7328551028997485470L;
        private final int numberOfOverdueBooks;
        public PatronHasOverdueBooks(final int numberOfOverdueBooks) {
            this.numberOfOverdueBooks = numberOfOverdueBooks;
        }
        public int getNumberOfOverdueBooks() {
            return this.numberOfOverdueBooks;
        }
    
    }

    In Library class, I added to the checkout method a validation for overdue books:

        public void checkout(final Long patronId, final Date checkoutDate, final Long... bookIds) {
            ...
            // Checking number of overdue books of the Patron
            int booksOverdue = getNumberOfOverdueBooksOfPatron(p, checkoutDate);
            if (booksOverdue != 0) {
                throw new PatronHasOverdueBooks(booksOverdue);
            }
    
            ...
        }

    Also, in Library class, the new method: getNumberOfOverdueBooksOfPatron

        public int getNumberOfOverdueBooksOfPatron(final Patron patron, final Date compareDate) {
            return getLoanDao().numberOfOverdueBookOfPatron(patron, compareDate);
        }

    LoanDao can return the number of overdue books of specific Patron:

        /**
         * Return number of overdue books of this patron
         *
         * @param patron
         * @param compareDate
         * @return int - The number of overdue books
         */
        public int numberOfOverdueBookOfPatron(final Patron patron, final Date compareDate) {
            final Query query = getEm().createNamedQuery("Loan.overdueBooksOfPatron");
            query.setParameter("date", compareDate);
            query.setParameter("patron", patron);
    
            return query.getResultList().size();
    
        }

    It uses a new named-query. One of the parameter is the Patron (not the patron’s id). I wanted to see how the join works …

    @NamedQuery(name = "Loan.overdueBooksOfPatron", query = "SELECT l.book FROM Loan l WHERE l.dueDate < :date AND l.patron = :patron")


  • Comments

    " Creative Commons License
    This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.