Introduction

This is the Yada Bookstore Tutorial. It serves two purposes: show you how quickly you can develop a web application, and how it compares to the recommended Oracle way of doing that. This chapter will therefore follow the official Java "Duke’s Bookstore Case Study Example" highlighting the many improvements in maintainability and functionality.

Original Oracle Java Bookstore tutorial links:

Rewrite the intro when finished.

Prerequisites

This tutorial assumes that you have a development environment available and configured by following the instructions in the Getting Started chapter.

git repo with the source code

Database Layer

The Yada Framework uses the Hibernate implementation of the Java Persistence API (JPA).

In short, the purpose of JPA is to represent the relational database (RDBMS) tables as Java objects, called "Entities". From an inverse perspective, the purpose of JPA is to store ("persist") Java objects into a relational database while hiding as much as possible the database-related concepts.

Entity classes

A "Book" is represented in Java as a class and in the database as a table. The java class must have a specific structure in order to provide the information needed to store its values in the table:

  • be annotated with @Entity

  • have a "unique identifier" field

  • optionally have a "version" field

  • use all the required field annotations that describe how the value is persisted

The developer can choose either to start from the class and derive the table, or start defining the table then derive the class. In the Yada Framework, the schema creation gradle task can create a schema from the java classes, as explained in the Database Layer section below.

Following the official Java Bookstore tutorial, the Book entity class is as follows:

import javax.persistence.Entity;
import javax.persistence.Table;
...

@Entity
@Table(uniqueConstraints = @UniqueConstraint(  (1)
		columnNames={"surname", "firstname", "title", "calendarYear"})
)
public class Book implements Serializable {
	private static final long serialVersionUID = 1L;
	@Version (2)
	private long version;
	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY) (3)
	private Long id; (3)
	private String surname;
	private String firstname;
	private String title;
	private Double price;
	private Boolean onsale;
	private Integer calendarYear;
	private String description;
	private Integer inventory;
	...
1 creates a unique constraint on four columns, to prevent storing two rows for the same book
2 addw a @Version attribute for optimistic locking: you won’t be able to store an object if its database value has been modified in the meanwhile
3 as an @Id usew a Long id that is autogenerated by the database

The database schema generator creates the following table definition:

create table Book (id bigint not null auto_increment, calendarYear integer, description varchar(255), firstname varchar(255), inventory integer, onsale bit, price double precision, surname varchar(255), title varchar(255), version bigint not null, primary key (id)) engine=InnoDB;
alter table Book add constraint UK9u3gktk5oqaaxduqbu8hql9yy unique (surname, firstname, title, calendarYear);

The database schema can be uploaded to the database with the /YadaBookstore/env/dev/dropAndCreateDatabase.bat script or equivalent.

The code in the official tutorial follows, with comments on the changes we made:

@Entity
@Table(name = "WEB_BOOKSTORE_BOOKS") (1)
@NamedQuery( (2)
        name = "findBooks",
        query = "SELECT b FROM Book b ORDER BY b.bookId")
public class Book implements Serializable {
    private static final long serialVersionUID = -4146681491856848089L; (3)
    @Id (4)
    @NotNull (5)
    private String bookId; (6)
    private String surname;
	...
1 the table name has been removed because the default table name "Book" seems good enough
2 the @NamedQuery annotation has been removed so that all queries are stored in the DAO (see later)
3 the serialVersionUID isn’t usually really used so the default value of 1L is enough and less confusing
4 the @Id has become a Long in order to use @GeneratedValue and have it created by the database: having to set the book id manually can work on a small example like this but is cumbersom in a real application, unless you’re using a real-life unique identifier like the ISBN code of the book
5 the @NotNull annotation is not required as the database will enforce a value
6 the name bookId has been replaced with id so that it’s easier to copy&paste the id definition to other entities

Data Access Objects (aka Repositories)

Data Access Objects (DAO) are classes that perform all database operations within a transaction. They must be annotated with @Repository and some other transaction-definition elements.

import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...

@Repository
@Transactional(readOnly = true) (1)
public class BookDao {
	private final transient Logger log = LoggerFactory.getLogger(getClass()); (2)

    @PersistenceContext private EntityManager em;

    @Transactional(readOnly = false) (1)
    public void createBook(String surname, String firstname,
        String title, Double price, Boolean onsale, Integer calendarYear,
        String description, Integer inventory) {
        Book book = new Book(surname, firstname, title, price,
                onsale, calendarYear, description, inventory);
        em.persist(book);
        log.info("Persisted book {}", title); (2)
    }

    public List<Book> getBooks() {
        return em.createQuery("FROM Book ORDER BY id", Book.class).getResultList(); (3)
    }
    ...
1 It is good practice to declare all methods as "read only" by default and only give write permission to the ones that actually write to the database
2 This is the logback syntax for declaring and using a log instance
3 The Book lookup query has been removed from the Entity and added here. You can still use a named query defined on the Entity, but in real life project you would still have to write the most complex queries in the DAO, resulting in confusion on the location of the sql code: better put everything in the DAO from the start

The full code can be found in git.

The original version of BookDao is implemented in BookRequestBean. This is an Enterprise Java Bean (EJB) but it’s not much different from our version:

@Stateful (1)
public class BookRequestBean {

    @PersistenceContext
    private EntityManager em;
    private static final Logger logger =
            Logger.getLogger("dukesbookstore.ejb.BookRequestBean");

    public BookRequestBean() throws Exception {
    }

    public void createBook(String bookId, String surname, String firstname,
            String title, Double price, Boolean onsale, Integer calendarYear,
            String description, Integer inventory) {  (2)
        try {
            Book book = new Book(bookId, surname, firstname, title, price,
                    onsale, calendarYear, description, inventory);
            logger.log(Level.INFO, "Created book {0}", bookId);
            em.persist(book);
            logger.log(Level.INFO, "Persisted book {0}", bookId);
        } catch (Exception ex) {
            throw new EJBException(ex.getMessage());
        }
    }

    public List<Book> getBooks() throws BooksNotFoundException { (3)
        try {
            return (List<Book>) em.createNamedQuery("findBooks").getResultList();
        } catch (Exception ex) {
            throw new BooksNotFoundException(
                    "Could not get books: " + ex.getMessage());
        }
    }
	...
1 We don’t need the EJB declaration
2 The DAO version doesn’t receive the book id on creation, because the id is computed by the database on save
3 We think that the original version makes excessive use of checked exceptions like BooksNotFoundException

Initial data

In the original Bookstore tutorial, some books are added to the database on application startup using the ConfigBean EJB where book definitions are hardcoded. We prefer a different approach: using the application configuration file. The /src/main/resources/conf.webapp.prod.xml file should be edited to store the initial book definitions. This file holds values for the production environment but these values are also used in any other lesser environment when the equivalent data is missing. So if the data should be the same in every environment, just add it to the production configuration and it will be seen everywhere.

<setup>
	<books>
		<book>
			<surname>Duke</surname>
			<firstname></firstname>
			<title>My Early Years: Growing Up on *7</title>
			<price>30.75</price>
			<onsale>false</onsale>
			<calendarYear>2005</calendarYear>
			<description>What a cool book.</description>
			<inventory>20</inventory>
		</book>
		<book>
			<surname>Jeeves</surname>
			<firstname></firstname>
			<title>Web Servers for Fun and Profit</title>
			<price>40.75</price>
			<onsale>true</onsale>
			<calendarYear>2010</calendarYear>
			<description>What a cool book.</description>
			<inventory>20</inventory>
		</book>
		...

All configuration elements are read by the YbsConfiguration class that should act as an interface between the xml world and the object world: a new getInitialBooks() will make the conversion and return a Book list with the values from the configuration:

public List<Book> getInitialBooks() {
	List<Book> result = new ArrayList<>();
	List<ImmutableHierarchicalConfiguration> booksSetup = configuration.immutableConfigurationsAt("config/setup/books/book");
	for (ImmutableHierarchicalConfiguration bookSetup : booksSetup) {
		String surname = bookSetup.getString("surname");
		String firstname = bookSetup.getString("firstname");
		String title = bookSetup.getString("title");
		Double price = bookSetup.getDouble("price");
		Boolean onsale = bookSetup.getBoolean("onsale", false); // Defaults to false
		Integer calendarYear = bookSetup.getInt("calendarYear");
		String description = bookSetup.getString("description");
		Integer inventory = bookSetup.getInt("inventory");
		Book book = new Book(surname, firstname, title, price, onsale, calendarYear, description, inventory);
		result.add(book);
	}
	return result;
}

Instead of using an EJB, we can add the startup code to the setupApplication() method of the …​/components/Setup.java class:

 @Override
 protected void setupApplication() {
	 List<Book> configuredBooks = config.getInitialBooks();
	 bookDao.createWhenMissing(configuredBooks);
 }

The new method createWhenMissing() of the BookDao class uses a MySQL native query to add a book row only when it doesn’t exist already. This is the fastest way of dealing with database initialization when giving the option of adding new <setup> entries in the future. If this is not a requirement, you can quickly skip book creation when at least one row is found in the database.

@Transactional(readOnly = false)
public void createWhenMissing(List<Book> configuredBooks) {
	String sql = "insert ignore into Book (surname, firstname, title, price, onsale, calendarYear, description, inventory) "
		+ "values (:surname, :firstname, :title, :price, :onsale, :calendarYear, :description, :inventory)"; (1)
	for (Book book : configuredBooks) {
		em.createNativeQuery(sql)
			.setParameter("surname", book.getSurname())
			.setParameter("firstname", book.getFirstname())
			.setParameter("title", book.getTitle())
			.setParameter("price", book.getPrice())
			.setParameter("onsale", book.getOnsale())
			.setParameter("calendarYear", book.getCalendarYear())
			.setParameter("description", book.getDescription())
			.setParameter("inventory", book.getInventory())
			.executeUpdate();
	}
}
1 insert ignore does the trick of skipping existing elements. It works because of the unique constraint that was added in the Book entity

The original hardcoded version is less maintainable and less flexible because it fails with an exception when the first book is already stored.

@Singleton
@Startup
public class ConfigBean {
    @EJB
    private BookRequestBean request;
    @PostConstruct
    public void createData() {
        request.createBook("201", "Duke", "",
                "My Early Years: Growing Up on *7",
                30.75, false, 2005, "What a cool book.", 20);
        request.createBook("202", "Jeeves", "",
                "Web Servers for Fun and Profit", 40.75, true,
                2010, "What a cool book.", 20);
		...
    }
}

Presentation Layer

General description

The presentation layer receives page requests from the browser and returns HTML ready for display. The returned HTML already contains any dynamic information that is specific for the parameters sent by the browser. So if the user is trying to open the description of a book, the id of the book is be sent in the request and the presentation layer returns the HTML containing the description of that specific book.

This is in contrast to how some other technologies work, where a generic HTML template is sent to the browser, then any request for specific data will return not a new HTML page but just the data that will be inserted in the existing template on the browser.

In the Yada Framework, web pages are coded in plain HTML that is made dynamic by using Thymeleaf attributes and tags. Dynamic values are taken from java beans that are added to the page "model" after being fetched from database. The class that receives and handles browser requests is called a "Controller".

Book Display

The Oracle Bookstore Tutorial starts with a page that shows an image of all available books on a 3x2 grid and a similar grid with only the book titles in text form.

Oracle Bookstore homepage
Figure 1. Tomcat configuration

Running the Yada Bookstore tutorial

The tutorial can either be deployed to a standalone Tomcat server or run with an embedded Tomcat. In the latter case, the command from the command line is the following:

java net.yadaframework.core.YadaTomcatServer ybsdev src/main/webapp