You're already happily storing MenuItem
data with the components shown on the following diagram:
Your next task for the Yummy Noodle Bar persistence project is to store Order data. Yummy Noodle Bar has decided to use PostgreSQL to store this data, a freely available, robust, relational database.
Continuing from the last section, your focus will be on developing components within the Persistence domain.
In that domain you have a component that represents an Order, com.yummynoodle.persistence.domain.Order
that you can optimise for persistence without affecting the rest of your application.
There is an event handler, OrderPersistenceEventHandler
, that exchanges events between the application core and the repository OrdersRepository
. The repository's responsibility is to persist and retrieve Order data for the rest of the application.
You will implement OrdersRepository
using Spring Data JPA and integrate this with the OrderPersistenceEventHandler
.
JPA is the standard Java mechanism for working with relational databases. JPA provides an API to map Java Objects to relational concepts and comes with a rich vocabulary for querying across these objects, transparently translating to SQL (most of the time).
Since JPA is a standard there are many implementations referred to as JPA Providers.
For this tutorial, you will use Spring Data JPA along with Hibernate as the JPA provider.
PostgreSQL is a fully functional database that is suitable for production. However for testing, a lighter, embedded database is suitable. You will use H2 for development purposes during this tutorial. H2 allows the easy creation and destruction of database instances in a lifecycle controlled by a test.
The JPA standard provides enough of an abstraction that you may use different databases in production and development.
We recommend to test application integration with the production database in addition to the interaction tests run against H2. This would normally be part of your minimal set of acceptance or 'smoke' tests.
Import Spring Data JPA and the Hibernate JPA Provider into your project by adding it to the build.gradle 's list of dependencies:
build.gradle
compile 'org.springframework.data:spring-data-mongodb:1.2.3.RELEASE'
compile 'org.springframework.data:spring-data-jpa:1.3.4.RELEASE'
compile 'org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.1.Final'
compile 'org.hibernate:hibernate-entitymanager:4.0.1.Final'
runtime 'com.h2database:h2:1.3.173'
In the above code you can also see the dependencies for H2.
Following the pattern from the previous section, first create a test to drive your development that checks that the persistence mapping class correctly stores and retrieves records.
First, create a placeholder configuration class com.yummynoodlebar.config.JPAConfiguration
.
Next, create a helper class com.yummynoodlebar.persistence.domain.fixture.JPAAssertions
, similar to the MongoAssertions
in the previous section; This uses both Hibernate and JDBC to inspect what has been done to the database schema. It does this to provide a set of test methods that make your tests to more readable and show their intent that much more clearly.
src/test/java/com/yummynoodlebar/persistence/domain/fixture/JPAAssertions.java
package com.yummynoodlebar.persistence.domain.fixture;
import org.hibernate.Session;
import org.hibernate.internal.SessionImpl;
import org.hibernate.jdbc.Work;
import javax.persistence.EntityManager;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import static junit.framework.TestCase.fail;
public class JPAAssertions {
public static void assertTableHasColumn(EntityManager manager, final String tableName, final String columnName) {
SessionImpl session = (SessionImpl) manager.unwrap(Session.class);
final ResultCollector rc = new ResultCollector();
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
ResultSet columns = connection.getMetaData().getColumns(null, null, tableName.toUpperCase(), null);
while(columns.next()) {
if (columns.getString(4).toUpperCase().equals(columnName.toUpperCase())) {
rc.found=true;
}
}
}
});
if (!rc.found) {
fail("Column [" + columnName + "] not found on table : " + tableName);
}
}
public static void assertTableExists(EntityManager manager, final String name) {
SessionImpl session = (SessionImpl) manager.unwrap(Session.class);
final ResultCollector rc = new ResultCollector();
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
ResultSet tables = connection.getMetaData().getTables(null, null, "%", null);
while(tables.next()) {
if (tables.getString(3).toUpperCase().equals(name.toUpperCase())) {
rc.found=true;
}
}
}
});
if (!rc.found) {
fail("Table not found in schema : " + name);
}
}
static class ResultCollector {
public boolean found = false;
}
}
Finally, create the new test class OrderMappingIntegrationTests
:
src/test/java/com/yummynoodlebar/persistence/integration/OrderMappingIntegrationTests.java
package com.yummynoodlebar.persistence.integration;
import com.yummynoodlebar.config.JPAConfiguration;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import static com.yummynoodlebar.persistence.domain.fixture.JPAAssertions.assertTableExists;
import static com.yummynoodlebar.persistence.domain.fixture.JPAAssertions.assertTableHasColumn;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {JPAConfiguration.class})
@Transactional
@TransactionConfiguration(defaultRollback = true)
public class OrderMappingIntegrationTests {
@Autowired
EntityManager manager;
@Test
public void thatItemCustomMappingWorks() throws Exception {
assertTableExists(manager, "NOODLE_ORDERS");
assertTableExists(manager, "ORDER_ORDER_ITEMS");
assertTableHasColumn(manager, "NOODLE_ORDERS", "ORDER_ID");
assertTableHasColumn(manager, "NOODLE_ORDERS", "SUBMISSION_DATETIME");
}
}
This test checks that com.yummynoodle.persistence.domain.Order
correctly maps to the JPA wrapped database (H2).
It is important to have an understanding of how your object is being mapped against the database and test that it meets your expectations. If you know how and why the mapping is occurring you can create indexes and other optimisations and be safe in the knowledge that the data is where you expect it to be. You may also access the data outside of the JPA provider, directly querying the database.
This test is making use of an existing helper class JPAAssertions
.
src/test/java/com/yummynoodlebar/persistence/integration/OrderMappingIntegrationTests.java
assertTableExists(manager, "NOODLE_ORDERS");
This line captures the expectation that the table NOODLE_ORDERS
exists. This is followed by the checks that assert the appropriate column structure.
If required, these tests could be extended to further check the schema definition to ensure that the data is being mapped as expected.
This test will not pass yet as the JPA infrastructure needed to connect Order
with the database has not been set up. To handle that, create a new configuration component in your application's Config domain calledJPAConfiguration
with the following settings:
src/main/java/com/yummynoodlebar/config/JPAConfiguration.java
package com.yummynoodlebar.config;
import com.yummynoodlebar.persistence.repository.OrdersRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.hibernate4.HibernateExceptionTranslator;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.sql.SQLException;
@Configuration
@EnableJpaRepositories(basePackages = "com.yummynoodlebar.persistence.repository",
includeFilters = @ComponentScan.Filter(value = {OrdersRepository.class}, type = FilterType.ASSIGNABLE_TYPE))
@EnableTransactionManagement
public class JPAConfiguration {
@Bean
public DataSource dataSource() throws SQLException {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setType(EmbeddedDatabaseType.H2).build();
}
@Bean
public EntityManagerFactory entityManagerFactory() throws SQLException {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(true);
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan("com.yummynoodlebar.persistence.domain");
factory.setDataSource(dataSource());
factory.afterPropertiesSet();
return factory.getObject();
}
@Bean
public EntityManager entityManager(EntityManagerFactory entityManagerFactory) {
return entityManagerFactory.createEntityManager();
}
@Bean
public PlatformTransactionManager transactionManager() throws SQLException {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory());
return txManager;
}
@Bean
public HibernateExceptionTranslator hibernateExceptionTranslator() {
return new HibernateExceptionTranslator();
}
}
The method DataSource dataSource()
creates the embedded H2 database. This creates a new H2 instance within the same ApplicationContext and provides a DataSource interface to it, usable by JPA.
The method EntityManagerFactory entityManagerFactory()
creates the EntityManagerFactory
. This class is responsible for creating the EntityManager
, and is JPA Provider specific. In this case, this shows the creation and setup of a Hibernate JPA Provider, including the provision of the datasource dataSource()
.
The EntityManagerFactory is responsible for identifying the JPA Entities to be made available, the classes to be treated as database mapping/ persistence beans.
The method EntityManager entityManager()
creates the core class of JPA. EntityManager
is the public interface of JPA, providing methods to persist, delete, update and query, and is used in the tests below for this purpose.
transactionManager()
initialises the JPA transaction manager. This integrates with the declarative Transaction Management features of Spring, permitting the use of @Transactional and associated classes and configuration, for more information, click here
Spring provides an exception translation framework to translate exceptions from many different sources into a consistent set that your application can use. In this case, the JPA Configuration expects a bean that provides these translations, which is provided by hibernateExceptionTranslator()
The test will now run without compilation or runtime errors but will fail as the JPA entity is not set up.
Update com.yummynoodle.persistence.domain.Order
to contain the following:
src/main/java/com/yummynoodlebar/persistence/domain/Order.java
package com.yummynoodlebar.persistence.domain;
import com.yummynoodlebar.events.orders.OrderDetails;
import javax.persistence.*;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
@Entity(name = "NOODLE_ORDERS")
public class Order {
@Column(name = "SUBMISSION_DATETIME")
private Date dateTimeOfSubmission;
@ElementCollection(fetch = FetchType.EAGER, targetClass = java.lang.Integer.class)
@JoinTable(name="ORDER_ORDER_ITEMS", joinColumns=@JoinColumn(name="ID"))
@MapKeyColumn(name="MENU_ID")
@Column(name="VALUE")
private Map<String, Integer> orderItems;
@Transient
private OrderStatus orderStatus;
@Id
@Column(name = "ORDER_ID")
private String id;
public void setId(String id) {
this.id = id;
}
public void setDateTimeOfSubmission(Date dateTimeOfSubmission) {
this.dateTimeOfSubmission = dateTimeOfSubmission;
}
public OrderStatus getStatus() {
return orderStatus;
}
public void setStatus(OrderStatus orderStatus) {
this.orderStatus = orderStatus;
}
public Date getDateTimeOfSubmission() {
return dateTimeOfSubmission;
}
public String getId() {
return id;
}
public void setOrderItems(Map<String, Integer> orderItems) {
if (orderItems == null) {
this.orderItems = Collections.emptyMap();
} else {
this.orderItems = Collections.unmodifiableMap(orderItems);
}
}
public Map<String, Integer> getOrderItems() {
return orderItems;
}
public OrderDetails toOrderDetails() {
OrderDetails details = new OrderDetails();
details.setKey(UUID.fromString(this.id));
details.setDateTimeOfSubmission(this.dateTimeOfSubmission);
details.setOrderItems(this.getOrderItems());
return details;
}
public static Order fromOrderDetails(OrderDetails orderDetails) {
Order order = new Order();
order.id = orderDetails.getKey().toString();
order.dateTimeOfSubmission = orderDetails.getDateTimeOfSubmission();
order.orderItems = orderDetails.getOrderItems();
return order;
}
}
@Entity(name = "NOODLE_ORDERS")
declares this class as a JPA Entity. This is a class that is mapped to a database and able to be consumed by EntityManager
.
@Column(name = "SUBMISSION_DATETIME")
is a JPA customisation that alters the name of the column this field will be mapped to. The default is the name of the field converted from lower camel (aFieldName) to uppsercase underscore case (A_FIELD_NAME)
@ElementCollection(fetch = FetchType.EAGER, targetClass = java.lang.Integer.class)
@JoinTable(name="ORDER_ORDER_ITEMS", joinColumns=@JoinColumn(name="ID"))
@MapKeyColumn(name="MENU_ID")
@Column(name="VALUE")
This mapping is used to create a joined table, ORDER_ORDER_ITEMS, that contains the data stored in the java.util.Map.
@Transient
informs JPA that the given field should not be stored in the database, and is analogous to the Java transient keyword
Finally @Id
indicates to both JPA and Spring Data that the given field(s) is the Primary Key, and should be used to both index and provide the default access method for the Entity.
Run your tests again and they will now pass, indicating that the mapping is all working as expected.
Now that your JPA Order entity is working properly, you can now implement a repository to work with your entity.
Taking the same approach that you applied earlier for MongoDB, Spring Data provides a way to automatically create JPA backed repositories and fully fledged queries given only an interface as a starting point.
Create a new test class to test the repository that you've not written yet:
src/test/java/com/yummynoodlebar/persistence/integration/OrdersRepositoryIntegrationTests.java
package com.yummynoodlebar.persistence.integration;
import com.yummynoodlebar.config.JPAConfiguration;
import com.yummynoodlebar.persistence.domain.Order;
import com.yummynoodlebar.persistence.repository.OrdersRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertNotNull;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {JPAConfiguration.class})
@Transactional
@TransactionConfiguration(defaultRollback = true)
public class OrdersRepositoryIntegrationTests {
@Autowired
OrdersRepository ordersRepository;
@Test
public void thatItemIsInsertedIntoRepoWorks() throws Exception {
String key = UUID.randomUUID().toString();
Order order = new Order();
order.setDateTimeOfSubmission(new Date());
order.setId(key);
Map<String, Integer> items = new HashMap<String, Integer>();
items.put("yummy1", 15);
items.put("yummy3", 12);
items.put("yummy5", 7);
order.setOrderItems(items);
ordersRepository.save(order);
Order retrievedOrder = ordersRepository.findById(key);
assertNotNull(retrievedOrder);
assertEquals(key, retrievedOrder.getId());
assertEquals(items.get("yummy1"), retrievedOrder.getOrderItems().get("yummy1"));
}
}
The following section is new:
src/test/java/com/yummynoodlebar/persistence/integration/OrdersRepositoryIntegrationTests.java
@Transactional
@TransactionConfiguration(defaultRollback = true)
These annotations integrate with the Spring declarative transaction management. The annotations state that every method on this class must be wrapped with a transaction and that the transaction should be, by default, rolled back on method completion.
Note: If you aren't familiar with database transactions they are a means to combine multiple database operations into a single, logical and atomic action. Either all the steps inside the transaction succeed or, upon any failure, are rolled back to the state before the transaction was started.
Spring transactions provide an incredibly convenient way to construct tests against a database by letting you automatically wrap a test method inside a transaction. Inside the test method, you update the database, reading and writing as your test requires. At the end of the test the transaction will be rolled back and the data discarded leaving a fresh environment for the next test to execute.
Your test requires an instance of OrdersRepository
and saves several Order instances to it before calling a findById
method. This will fail, as the implementation of OrdersRepository
does not yet exist.
To solve this, update JPAConfiguration
to enable the JPA Repository system. The desired repository is selected explicitly as multiple data sources/Spring Data configurations are being used.
src/main/java/com/yummynoodlebar/config/JPAConfiguration.java
@Configuration
@EnableJpaRepositories(basePackages = "com.yummynoodlebar.persistence.repository",
includeFilters = @ComponentScan.Filter(value = {OrdersRepository.class}, type = FilterType.ASSIGNABLE_TYPE))
@EnableTransactionManagement
public class JPAConfiguration {
Then replace the contents of OrdersRepository
to extend the Spring Data CrudRepository
:
src/main/java/com/yummynoodlebar/persistence/repository/OrdersRepository.java
package com.yummynoodlebar.persistence.repository;
import com.yummynoodlebar.persistence.domain.Order;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface OrdersRepository extends CrudRepository<Order, String> {
Order findById(String key);
The test will now pass correctly indicating that an implementation of OrderRepository
is being created at runtime by Spring Data JPA and works as expected.
Users need to be able to find Orders that contain certain menu items by Menu Item ID.
Create a new test OrdersRepositoryFindOrdersContainingTests
to look for this new functionality:
src/test/java/com/yummynoodlebar/persistence/integration/OrdersRepositoryFindOrdersContainingTests.java
package com.yummynoodlebar.persistence.integration;
import com.yummynoodlebar.config.JPAConfiguration;
import com.yummynoodlebar.persistence.domain.Order;
import com.yummynoodlebar.persistence.domain.fixture.PersistenceFixture;
import com.yummynoodlebar.persistence.repository.OrdersRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.util.List;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertNotNull;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {JPAConfiguration.class})
@Transactional
@TransactionConfiguration(defaultRollback = true)
public class OrdersRepositoryFindOrdersContainingTests {
@Autowired
OrdersRepository ordersRepository;
@Autowired
EntityManager entityManager;
@Test
public void thatSearchingForOrdesContainingWorks() throws Exception {
ordersRepository.save(PersistenceFixture.standardOrder());
ordersRepository.save(PersistenceFixture.standardOrder());
ordersRepository.save(PersistenceFixture.yummy16Order());
ordersRepository.save(PersistenceFixture.yummy16Order());
List<Order> retrievedOrders = ordersRepository.findOrdersContaining("yummy16");
assertNotNull(retrievedOrders);
assertEquals(2, retrievedOrders.size());
assertEquals(22, (int) retrievedOrders.get(0).getOrderItems().get("yummy16"));
retrievedOrders = ordersRepository.findOrdersContaining("yummy3");
assertNotNull(retrievedOrders);
assertEquals(2, retrievedOrders.size());
}
}
Once again, this test is a transactional database test that indicates that the database will roll back any changes on test conclusion. The test saves a set of orders and performs two queries using a method findOrdersContaining
that will accept a menu item ID.
Open the repository and update it with the new search method:
src/main/java/com/yummynoodlebar/persistence/repository/OrdersRepository.java
@Query(value = "select no.* from NOODLE_ORDERS no where no.ORDER_ID in (select ID from ORDER_ORDER_ITEMS where MENU_ID = :menuId)", nativeQuery = true)
List<Order> findOrdersContaining(@Param("menuId") String menuId);
This class uses a custom @Query. The annotation contains a SQL query, for which you have to set nativeQuery=true. Without `nativeQuery=true', the string in @Query is assumed to be a JPA Query Language query instead.
The full OrdersRepository
class should now contain:
src/main/java/com/yummynoodlebar/persistence/repository/OrdersRepository.java
package com.yummynoodlebar.persistence.repository;
import com.yummynoodlebar.persistence.domain.Order;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface OrdersRepository extends CrudRepository<Order, String> {
Order findById(String key);
@Query(value = "select no.* from NOODLE_ORDERS no where no.ORDER_ID in (select ID from ORDER_ORDER_ITEMS where MENU_ID = :menuId)", nativeQuery = true)
List<Order> findOrdersContaining(@Param("menuId") String menuId);
}
Finally your test will now pass and the custom query is completed. You can run the entire test suite using Gradle:
$ ./gradlew test
Order data is safely stored and retrievable using a JPA managed relational database. You've added an OrdersRepository
component and a JPAConfiguration
component to your application, as shown by the following Life Preserver diagram:
Next, you can see how to store OrderStatus
data in the GemFire distributed data grid.
Next… Storing the Order Status in Gemfire using Spring Data GemFire