Enterprise Java Development@TOPIC@
Built on: 2019-08-22 07:12 EST
Copyright © 2019 jim stafford (jim.stafford@jhu.edu)
Abstract
This book contains course notes covering Enterprise Computing with Java. This comprehensive course explores a variety of modern Java frameworks and technologies that can be used for developing mission-critical complex enterprise applications. The emphasis is on use of the latest Java EE platform, its set of underlying specifications, designing and developing server-side application components. Students will learn thru having hands-on experience in building multi-tier distributed enterprise applications, comparing and using variety of Java EE design patterns, rich set of server-side components and technologies, and web-enabled by modern design practices and communication protocols. Students will learn to:
Implement a data access tier to a relational database using Java Persistence API (JPA), Java Database Connectivity (JDBC) API, and variety of data modeling and access patterns.
Implement synchronous and asynchronous server-side business logic using stateless and stateful session EJBs, message-driven EJBs and the EJB Timer service.
Integrate server-side logic with the web-tier components using legacy server-side and more modern RESTful API approaches that include JSON and XML.
Other critical Java EE infrastructure services will be discussed, including the Java Naming and Directory Interface (JNDI), the Java Message Service (JMS), the Java Transaction (JTA) API, and Java EE security. Using modern development tools, commercial persistence providers, and application servers, students will design and implement several significant programming projects using the above-mentioned technologies and deploy them to a Java EE environment that they will manage.
The course is being actively updated to JavaEE 8 (JPA 2.2, EJB 3.2, JAX-RS 2.1, and JMS 2.0) but a few order APIs and legacy examples remain. The development environment used in class is based on the Wildfly application server and is JavaSE 8 and JavaEE 7-compliant with the ability to preview some JavaEE 8-specific features.
Table of Contents
This comprehensive course explores a variety of modern Java frameworks and technologies that can be used for developing mission-critical complex enterprise applications. The emphasis is on use of the latest Java EE platform, its set of underlying specifications, designing and developing server-side application components. Students will learn thru having hands-on experience in building multi-tier distributed enterprise applications, comparing and using variety of Java EE design patterns, rich set of server-side components and technologies, and web-enabled by modern design practices and communication protocols. Students will learn to:
Implement a data access tier to a relational database using Java Persistence API (JPA) and variety of data modeling and access patterns.
Implement synchronous and asynchronous server-side business logic using stateless and stateful session EJBs, message-driven EJBs and the EJB Timer service.
Integrate server-side logic with the web-tier components using legacy server-side and more modern RESTful API approaches that include JSON and XML.
Other critical Java SE/EE infrastructure services will be discussed, including the Java Database Connectivity (JDBC) API, Java Naming and Directory Interface (JNDI), the Java Message Service (JMS), the Java Transaction (JTA) API, and Java EE security. Using modern development tools, commercial persistence providers, and application servers, students will design and implement several significant programming projects using the above-mentioned technologies and deploy them to a Java EE environment that they will manage.
The course is being actively updated to JavaEE 8 (JPA 2.2, EJB 3.2, JAX-RS 2.1, and JMS 2.0) and JDK 11 but a few order APIs and legacy examples remain for demonstration purposes. The development environment used in class is based on the Wildfly application server and is JDK 11 and JavaEE 8-compliant.
Prerequisite: 605.481 Distributed Development on the World Wide Web or equivalent
Strong Java programming skills are assumed.
Students should be prepared to spend between 10-16 hours a week outside of class. Time spent can be made efficient by proactively keeping up with class topics and actively collaborating with the instructor and other students in the course.
The course uses no mandatory texts. The course examples and notes for this course and free Internet resources are plentiful.
This course will make heavy use of development tools (JDK 11, Git, Maven 3, JUnit, SLF/Log4j, and Eclipse), a database (H2 Database Engine), JPA persistence provider (Hibernate), and application server (JBoss/Wildfly 17). Students are required to establish a local development environment. Detailed instructions for setup are part of the first exercise (Development Environment Setup).
Documentation will be provided through course slides, examples, and tutorials. Each lecture will supply a list of links for that technical area.
100 >= A >= 90 > B >= 80 > C >= 70 > F
Table 1.1.
Assessment | % of Semester Grade |
---|---|
Class/Newsgroup Participation | 10% |
Project Startup/Sanity Check | 5% |
Project 1 | 30% |
Project 2 | 30% |
Project 3 | 25% |
Projects will be done individually and graded 100 though 0 based on posted project grading criteria.
Class/newsgroup participation will be based on instructor judgment whether the student has made a contribution to class to either the classroom or newsgroup on a consistent weekly basis. A newsgroup contribution may be a well-formed technical observation/lesson learned, a well formed question that leads to a well formed follow up from another student, or a well formed answer/follow-up to another student's question. Well formed submissions are those that clearly summarize the topic in the subject, and clearly describe the objective, environment, and conditions in the body. The instructor will be the judge of whether a newsgroup contribution meets the minimum requirements for the week. The intent of this requirment is to promote public collaboration between class members.
There will be one required "project startup/sanity check" submission and will be primarily graded on a done/not-done basis. The intent of this requirement is to promote early, more-focused productivity and exchange of artifacts between the student and instructor.
Late projects will be deducted 10pts/week late, starting after the due date/time, with one exception. A student may submit a single project up to 4 days late without receiving approval and still receive complete credit. Students taking advantage of the free first pass should still submit an e-mail to the instructor and grader(s) notifying them of their intent.
Projects must be submitted via e-mail to the instructor and grader(s) with source code in a zip file with a README (links to archives on cloud storage acceptable as well). All source code must be written to portably compile in the grader's environment using Maven 3. This will be clearly spelled out during the course and you will have plenty of opportinuties to test the process prior to submitting a project for grade.
Class attendance is strongly recommended, but not mandatory. The student is responsible for obtaining any written or oral information covered during their absence.
Collaboration of ideas and approaches are strongly encouraged. You may use partial solutions provided by others as a part of your project submission. However, the bulk usage of another students implementation or project will result in a 0 for the project. There is a difference between sharing ideas/code snippets and turning in someone else's work as your own. When in doubt, document your sources.
Do not host your course project in a *public* Internet repository.
I am available at least 20min before class, breaks, and most times after class for extra discussion. I provide detailed answers to project and technical questions through the course newsgroup. You can get individual, non-technical questions answered via e-mail. It is very common for me to ask for a copy of your broken project so that I can provide more analysis and precise feedback. This is commonly transmitted either as an attachment to the e-mail or a link to an archive in GoogleDocs. Students needing further assistance are also welcome make other arrangements during the week or schedule a web meeting using Zoom Conferencing.
Table 2.1. Data Tier
Session | Date | Topic | Comment |
---|---|---|---|
1 | Aug28 | Course Introduction | |
2 | Sep04 | ||
3 | Sep11 | ||
Project 0 Startup/Sanity Check due by Sun, 8am Sep22 | |||
4 | Sep18 | ||
5 | Sep25 | ||
6 | Oct02 | ||
Project 1 - due by Sun, 8am Oct13 | |||
7 *NO CLASS* Online Meetups | Oct09 | Project 1 Topics |
Table 2.2. N-Tier Application/Enterprise JavaBeans
Session | Date | Topic | Comment |
---|---|---|---|
8 | Oct16 | ||
9 | Oct23 | ||
10 | Oct30 | ||
11 | Nov06 | ||
Web-UI integration | |||
Project 2 due by Sun, 8am Nov17 | |||
*** | Nov08 | EP Drop/Audit Deadline |
Table 2.3. Security and Asynchronous Applications
Session | Date | Topic | Comment |
---|---|---|---|
12 | Nov13 | ||
13 | Nov20 | ||
Nov28 | Thanksgiving Break (no class) | ||
14 | Dec04 | ||
Project 3 due by Sun, 5pm Dec09 |
Project 0 will require construction of your development tree and early implemementation of technical requirements for class projects. The intent of this short/skeletal project is to get the student to a concrete starting point for course topics, eliminate wrong assumptions, and to generate early discussions about the development and development options/trade-offs. Grading will be primarily a done/not-done grade with unsatisfactory starts given feedback and a request to re-submit.
Specific Goals:
Instantiate a Maven development tree for projects 1 thru 3.
Setup JUnit test case(s).
Develop business logic interfaces and method signatures for end-to-end scenario.
Develop skeletal business objects used in the business logic interfaces.
Develop an early JUnit test that demonstrates understanding of the end-to-end requirements for project 1.
Project 1 will require the development of the business objects, O/R mapping of the business objects to a relational database, and business logic to integrate the end-to-end application. The artifacts from project 1 will also be used as a foundation for all follow-on projects as well. There is no application server used in project 1. All code is executes within a single JVM and database.
Specific Goals:
Develop and test a Java application
Develop and test a set of POJOs
Design a relational database schema
Develop and test a DAO using JPA Entities
Develop and test a business logic to manipulate data using DAOs.
Ingest test data into database
Project 2 deploys the business logic and data tier to the server-side to be hosted in an EJB tier; providing resource management (e.g., threads), transactions, security (added in project 3), and remote access. A minimal user interface will be supplied through the use of Servlets and JSPs integrated with the EJBs. A remote client will also be developed for the unit tests. Students familiar with other UI frameworks make use alternative techniques but no additional credit will be gained with additional UI complexity or functionality.
Specific Goals:
Deploy business logic within an EJB application using Session Beans
Configure applications using JNDI and CDI.
Implement HTTP remote access to services using JAX-RS, JSON, and XML.
Integrate application with JTA
Integrate business logic with a Web tier
Project 3 enacts security for Project 2 and adds publish/subscribe topics and timers. An application server is used to host the application logic and JMS components. A stand-alone client is also developed to interface with the application through JMS messages.
Specific Goals:
Protect access to applications using Java EE security
Authenticate users using Java EE security means
Design asynchronous interactions
Design JMS messages
Develop and test MDB and stand-alone JMS clients
Develop and test EJB Timers
Projects must build using Maven 3. Ant will be used late in the semester to wrap any JavaSE commands with classpaths
All projects must be portable to build and deploy within grader and intructor environments. You may submit partial projects early to get portability feedback (not early content grading feedback).
Test Cases must be written using JUnit or another Java-based unit test framework that will run within a Maven surefire and failsafe environments.
Projects must be supplied with a README that points out how project meets requirements.
You should test your application prior to submission by
Run maven clean, zip your project from the root, and unzip in a new location. Move your localRepository (or set your settings.xml#localRepository value to a new location -- don't delete your primary localRepository) and run mvn clean install from the root of your project. This will make sure you do not have dependencies on older versions of your modules or manually installed artifacts. This will also make sure you are not depending on any old .class files. This, of course, will download all project dependencies and help verify that the project will build in other environments. This will also simulate what the grader will see when they unzip your project.
Coldstart your database instance(s) and run mvn clean install from the root of your project. This will make sure you are not depending on any residue schema or data in your database.
Make sure the README documents all information required to navigate your application (primarily a project 2 and 3 requirement) and point out issues that would be important for the grader/instructor to know (e.g., "the instructor said...")
You will e-mail the projects to the grader and me with the following subject line
(your name) project #; revision 0; part # of #
Your submission will include source zip (link to cloud storage is acceptable) and README (could be in source zip). You may also include pre-built artifacts if it does not add a huge size burden. The easiest way to do this is to zip up the project from the root after completing a build.
If you need to make a correction, the correction should have the following e-mail subject. The body should describe what you wish to revise.
* (your name) project #; revision N; part # of #
Submission e-mails (mail to all):
Jim - jim.stafford@jhu.edu
Table of Contents
Understand the purpose of enterprise applications
Understand the role of a container
Introduce the overall architecture for JavaEE
Introduce the standards contained within the JavaEE framework
At the completion of this topic, the student shall be able to:
Identity characteristics of enterprise applications
Identify the difference between a JavaEE API, JavaEE provider, and a JavaEE module
Identify the different container types for JavaEE
Identify the standards that make up a JavaEE
Core Language
Formerly called J2SE. Changed started with JavaSE 6
Java APIs are what applications are written against.
Standardized
Java Runtime Edition (JRE) is what applications run with.
Many providers
Java Developer Kit (JDK) is what applications are built with
Many providers
JRE and JDK provider choices are independent
Formerly called J2ME
Targeted at smaller devices
Small footprint
Limited power
Limited and sporadic bandwidth
Formerly called J2EE. Change started with JavaEE 5
Movement from Oracle to Eclipse Foundation after/at JavaEE 8
Temporarily called EE4J (Eclipse Enterprise for Java)
Has officially become Jakarta EE
Targeted at enterprise deployments commonly deployed to data centers
Persistence/Data Access
Scaling
Transactions
Security
Resource Management
Availability
Distributed
Web-based
Generally deployed in N-Tiers
Interface
Transacted Business Logic
Persistence/Back-end Integration
Contains
APIs (standardized)
Compatibility test suites
Reference Implementation (Glassfish)
Providers (e.g., Glassfish, JBoss/Wildfly, WebSphere, WebLogic, Payara)
Subset of JavaEE APIs targeted at key use cases
Share common underlying APIs for consistency
Provide smaller gates of entry for providers
Provide means to eliminate deprecated specs from concern
Risk fragmenting community and introducing confusion -- not taken lightly
WAR deployments
Only JavaEE profile to date
Noted Exclusions
Full EJB support, including remote interfaces and legacy 2.x capabilities
JMS and Message-Driven Beans (MDBs)
Web Service Standards
Not yet officially pay of Jakarta EE but part of Eclipse Foundation (Eclipse MicroProfile)
Micro-Container-focused services
Config
Metrics
Fault Tolerance
JAX-RS
...
Different types
Deployed to client (Applets)
Deployed to data center (Servlets and EJBs)
Undefined deployment (App Clients)
GUI components typically running in browser
Access middle-tier indirectly thru Web container and HTTP
Generate content to browser from Web container
Can be UI/HTML-based
Can be service/XML/JSON (or other structured data forms)-based
May contain EJBs providing transactional support
Provide runtime support for JavaEE application components
Provide an interpose layer around/between each component
Inject required components
Application developers may think of the Application Server==Container
Containers address specifics of the standards (e.g., deployment artifact structure)
System-level software component
Extends standard APIs into specific resources (e.g., JDBC)
Framework provided for:
Connection/Resource Pooling
Security Management
Transaction Management
Table 7.1. JavaEE 8 Specifications [2] [3]
Enterprise Application Technologies | Version | Web Profile | Course Relevance |
---|---|---|---|
Enterprise JavaBeans (EJB) | 3.2 | EJB-lite | Primary |
Java Persistence API (JPA) | 2.2 | Y | Y |
Context and Dependency Injection (CDI) | 2.0 | Y | Y |
Dependency Injection for Java (JSR-330) | 1.0 | Y | Y |
Common Annotations for the Java Platform | 1.3 | Y | Y |
Java Transaction API (JTA) | 1.2 | Y | Y |
Java Message Service API (JMS) | 2.0 | Y | |
Bean Validation | 2.0 | Y | Y |
Interceptors | 1.2 | Y | Y |
Managed Beans | 1.0 | Y | |
JavaEE Connector Architecture | 1.7 | ||
JavaMail | 1.6 | ||
Batch Applications for the Java Platform | 1.0 | ||
Concurrency Utilities for Java EE | 1.0 |
Web Service Technologies | Version | Web Profile | Course Relevance |
---|---|---|---|
Java API for RESTful Services (JAX-RS) | 2.1 | Y | Y |
Java Architecture for XML Binding (JAXB) | 2.2 | Y | |
JSON-B | 1.0 | Y | |
JSON-P | 1.1 | Y | (maybe) |
Java API for XML-based Web Services (JAX-WS) | 2.2 | ||
Web Services Metadata | 2.1 | ||
Java API for XML-based RPC (JAX-RPC) | 1.1 | ||
Java API for XML Registries (JAXR) | 1.0 | ||
Web Services for JavaEE | 1.4 | ||
Web Socket | 1.1 |
Web Related Technologies | Version | Web Profile | Course Relevance |
---|---|---|---|
Servlet | 4.0 | Y | Minor |
JavaServer Pages (JSP) | 2.3 | Y | Minor |
JavaServer Faces (JSF) | 2.3 | Y | |
Expression Language | 3.0 | Y | |
JSTL | 1.2 | Y |
Management Technologies | Version | Web Profile | Course Relevance |
---|---|---|---|
Java Authentication Service Provider Interface for Containers (JASPIC) | 1.1 | ||
Java Authorization Service Provider Contract for Containers (JACC) | 1.5 | ||
Java EE Management | 1.1 | ||
Java EE Deployment | 1.2 | ||
Debugging Support for Other Languages | 1.0 | Y |
[4]
1.2
"CORBA Integration"; Dec 1999
RMI over IIOP
1.3
"Local Interface EJBs"; Sep 2001
Connector API
EJB (2.x) local interfaces and new CMP model
1.4
"Web Service Enabled"; Nov 2003
SOAP Web Services
Deployment, Management, JAAS
5
"Ease of Development"; May 2006
Annotations
Better Defaults
EJB and Servlet Dependency Injection
JPA, StAX, JAX-WS
6
"More Ease of Development"; Dec 2009
Profiles; making some components optional
Flexible deployments; no EJB.jar or EAR requirement
No interface EJBs
Component and Dependency Injection
7
"Offloading Additional Common Infrastructure Tasks and More Ease of Development"; June 2013
Additional Web standards; Web Sockets and JSON
HTTP Client API within JAX-RS
Batch API
Concurrency Utilities
More consistency between CDI and component models
More inherent integrations with Validation API
Simplified JMS API
JPA Schema generation
8
"Focus on modern web applications";August 2017
Expanding support of JSON with JSON-B
HTTP/2 protocol
JAX-RS server-sent events
API for identity stores
...
Made a technical case for JavaEE and EJB
Described an envolution of EJB
Showed where EJB fits within the JavaEE Specs
Introduced 4-5 types of EJBs
Introduced 3 EJB deployment types
"JSR-000366 JavaTM Platform, Enterprise Edition 8 (Java EE 8) Specification (Final Release)", Linda DeMichiel, Bill Shannon, https://jcp.org/aboutJava/communityprocess/final/jsr366/index.html, 7/31/17 Oracle America, Inc.
"JavaEE Version History", Wikipedia, http://en.wikipedia.org/wiki/Java_EE_version_history, September 3. 2012.
Table of Contents
Become familiar with the development environment used with class and commonly used for enterprise Java developments
At the completion of this topic, the student shall
have more understanding of:
Maven project structure
Maven project pom.xml
Maven/Eclipse integration
Eclipse development tasks
be able to:
Establish a Maven project structure
Create and execute a test for Java class
within Eclipse
from Eclipse, within Maven
Debug a Java test and its associated class
within Eclipse
from Eclipse, within Maven (remote debugging)
Follows a standard layout by convention
Can be customized
Root directory stays fairly clean
|-- pom.xml |-- src | |-- main | `-- test `-- target |-- classes |-- ex1-1.0-SNAPSHOT.jar |-- surefire-reports `-- test-classes
src
Source tree for all developed code
target
Build tree for artifacts created by the build
pom.xml
Defines the build
Managed by CM along with src tree
Related artifacts normally placed in src/compile, etc.
Managed by CM
Should not contain any auto-generated artifacts
src |-- main | |-- java | | `-- myorg | | `-- mypackage | | `-- ex1 | | `-- App.java | `-- resources `-- test |-- java | `-- myorg | `-- mypackage | `-- ex1 | `-- AppTest.java `-- resources `-- log4j.xml
main
Contains production artifacts for module
Built artifacts from this tree are generally placed in the produced archive
jar tf target/ex1-1.0-SNAPSHOT.jar META-INF/ META-INF/MANIFEST.MF myorg/ myorg/mypackage/ myorg/mypackage/ex1/ myorg/mypackage/ex1/App.class ...
test
Contains test artifacts for module
Built artifacts from this tree are not generally placed in the produced archive
A separate archive of built test artifacts can be optionally generated for reuse
[main or test]/java
Contains Java classes
Placed in a directory that matches the Java package for the class
[main or test]/resources
Contains property or other data files
Generally copied to target tree as-is or optionally filtered by the build
Resource filtering is when substitution ${placeholder}s in source files are replaced by their declared property values in the target tree. This is quite useful in customizing URLs, credentials, and other properties that might be unique to the current build environment. Properties can be defined in the pom.xml, settings.xml or injected into the build through various plugins (e.g., inject current date).
Not managed by CM
Contains auto-generated artifacts
Can be deleted at any time
target |-- classes | `-- myorg | `-- mypackage | `-- ex1 | `-- App.class |-- ex1-1.0-SNAPSHOT.jar |-- log4j-out.txt |-- surefire-reports | |-- myorg.mypackage.ex1.AppTest.txt | `-- TEST-myorg.mypackage.ex1.AppTest.xml `-- test-classes |-- log4j.xml `-- myorg `-- mypackage `-- ex1 `-- AppTest.class
classes
An exploded directory tree for built artifacts to be placed into the module archive
Mixture of Java and resource artifacts
test-classes
An exploded directory tree for built artifacts used for test
Mixture of Java and resource artifacts
Part of the test classpath
*-reports
Results of unit and integration testing
surefire-reports
unit test results
failsafe-reports
integration test results
Locate details of test pass/failures within this directory
$ mvn clean test ... Failed tests: testFail(myorg.mypackage.ex1.AppTest): app didn't return 0 Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ ... $ more target/surefire-reports/*.txt ------------------------------------------------------------------------------- Test set: myorg.mypackage.ex1.AppTest ------------------------------------------------------------------------------- Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.608 sec <<< FAILURE! testFail(myorg.mypackage.ex1.AppTest) Time elapsed: 0.019 sec <<< FAILURE! java.lang.AssertionError: app didn't return 0 at org.junit.Assert.fail(Assert.java:93) at org.junit.Assert.assertTrue(Assert.java:43) at myorg.mypackage.ex1.AppTest.testFail(AppTest.java:28) ...
{artifactId}-{version}.jar
Results of unit and integration testing
Archive with contents of target/classes
Can be installed into local Maven repo by build
log4j-out.txt
An example output file of the build/tests written to the target directory
Produced by log4j based on configuration in src/test/resources/log4j.xml or log4j.properties
Defines the artifact for dependents
Defines the build of the local module
<?xml version="1.0"?>
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>myorg.myproject</groupId>
<artifactId>ex1</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>My First Simple Project</name>
...
<properties>
...
</properties>
<dependencies>
...
<dependencies>
<build>
<plugins>
...
</plugins>
</build>
</project>
Defines the type of module and automatically enacts a profile of plugs (default=jar)
packaging=jar - primary artifact is a Java archive
packaging=ejb - primary artifact is a JavaEE EJB
packaging=war - primary artifact is a JavaEE WAR
packaging=ear - primary artifact is a JavaEE EAR
packaging=pom - parent pom, used primarily for grouping leaf modules
packaging=... - custom extensions
Value assigned relates to dependency type
element
<dependency> <groupId>${project.groupId}</groupId> <artifactId>ejbsessionBankEAR</artifactId> <version>${project.version}</version> <type>ear</type> <scope>compile</scope> </dependency>
Used to uniquely identify the module
<groupId>myorg.myproject</groupId>
<artifactId>ex1</artifactId>
<version>1.0-SNAPSHOT</version>
groupId
Hierarchical name used to group a set of one or more artifacts
Represented as a directory structure in repositories
myorg/ `-- myproject `-- ex1 |-- 1.0-SNAPSHOT | |-- ex1-1.0-SNAPSHOT.jar | |-- ex1-1.0-SNAPSHOT.pom ...
artifactId
Base name for the artifacts produced by this module
1.0-SNAPSHOT/ |-- ex1-1.0-SNAPSHOT.jar |-- ex1-1.0-SNAPSHOT.pom |-- ex1-1.0-SNAPSHOT-tests.jar
version
Version ID for the artifact
Release: 1.0
Snapshot: 1.0-SNAPSHOT
Timestamped release that does not change name
Useful for active development not yet ready for release
Dependents poll for new snapshots and get the latest version
Easy to use during development but nearly impossible to manage changes
-SNAPSHOT suffix added to version
<version>1.0-SNAPSHOT</version>
1.0-SNAPSHOT/ |-- ex1-1.0-SNAPSHOT.jar
Locations where Maven stores and locates artifacts
Each user has a local repository referenced by ${settings.localRepository}
.
This defaults to $HOME/.m2/repository
<settings xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository>c:/jhu/repository</localRepository>
...
MAVEN_HOME/conf/settings.xml defines a core set of remote repositories
Maven poms can define extensions to the remote repository list
<repositories>
<repository>
<id>jboss-nexus</id>
<name>JBoss Nexus Repository</name>
<url>https://repository.jboss.org/nexus/content/groups/public-jboss/</url>
</repository>
</repositories>
Maven will first look for a dependency artifact in the local repository
Maven will then look for a dependency artifact in a remote repository like ibiblio
Maven will search all known repositories until the artifact is found or all repositories have been checked
Maven will install module artifacts into the local repository during the install
phase of the build
$ mvn install ... [INFO] --- maven-install-plugin:2.3.1:install (default-install) @ ex1 --- [INFO] Installing /home/jcstaff/proj/ejava-javaee/solutions/ex1/target/ex1-1.0-SNAPSHOT.jar to /home/jcstaff/.m2/repository/myorg/myproject/ex1/1.0-SNAPSHOT/ex1-1.0-SNAPSHOT.jar [INFO] Installing /home/jcstaff/proj/ejava-javaee/solutions/ex1/pom.xml to /home/jcstaff/.m2/repository/myorg/myproject/ex1/1.0-SNAPSHOT/ex1-1.0-SNAPSHOT.pom [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
-U
to force an update checkMaven will limit its search of a remote repository to
expressed time limits. If it attempts and fails to locate
an artifact from a remote repository it will not re-attempt
to query that repository until the time limit expires.
Sometimes the failure has nothing to do with the repository
not having the artifact (e.g., wireless network down) and
when you clear the error you can force Maven to retry before
the timeout period expires by adding a -U
to the command line.
-o
offline flag to eliminate repository checksMany times Maven will be too aggressive searching for artifacts you already know are not of value to the current build. You can tell Maven to work in an offline mode to bypass the work involved. This is helpful when you are disconnected from the network but will fail if you are truly missing a required artifact. You can activate/deactivate the offline more consistently using an element in the settings.xml
<offline>false</offline>
dependency:go-offline
prior to network/repo outageMaven will lazily download required artifacts on-demand as different
plugins are executed. If you know you will be separated from the network or
a key repository will have an upcoming outage -- you can prepare your local
environment by having Maven eagerly resolve and download all dependencies
using the dependency:go-offline
goal.
$ mvn dependency:go-offline
[INFO] Scanning for projects...
Provide a means to define behavior of the build
Provide a means to define values once and re-use across the build
Promotes consistency
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven-compiler-plugin.version>3.7.0</maven-compiler-plugin.version>
<java.source.version>1.8</java.source.version>
<java.target.version>1.8</java.target.version>
</properties>
Properties can also be expressed using -Dkey=value
system properties
Modules depend on artifacts from other modules
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
...
</dependencies>
GAV
Must provide its groupId, artifactId, and version for each dependency
scope
Defines when the dependency applies (default=compile)
scope=compile - classpath of src/main, src/test, and dependents
scope=provided - classpath of src/main, src/test but not dependents
scope=test - classpath of src/test but not src/main and dependents
scope=runtime - classpath of src/test and dependents but not of src/main
scope=import - used to replace locally declared dependencyManagement dependencies with the contents from another pom. Useful when declaring complex dependencies and want to make easy wholesale changes in dependency versions.
scope=system - used to declare a direct reference to a local archive. Not portable.
type
Defines the artifact type of ther module (default=jar)
type=jar - java archive
type=ejb - javaee EJB archive
type=war - javaee WAR archive
type=ear - javaee EAR archive
type=... - custom type
Value directly relates to the project packaging element of the dependency
|-- ejbsessionBankImpl | |-- ejbsessionBankImpl-3.0.2013.2-SNAPSHOT.jar |-- ejbsessionBankEJB | |-- ejbsessionBankEJB-3.0.2013.2-SNAPSHOT.jar |-- ejbsessionBankWAR | |-- ejbsessionBankWAR-3.0.2013.2-SNAPSHOT.war |-- ejbsessionBankEAR | |-- ejbsessionBankEAR-3.0.2013.2-SNAPSHOT.ear
classifier
Defines a special type of artifact/custom from a module
classifier=tests - java archive containing test artifacts
classifier=sources - java archive containing module source code
classifier=... - custom classifiers
`-- target ... |-- txHotelDAO-3.0.2013.2-SNAPSHOT.jar |-- txHotelDAO-3.0.2013.2-SNAPSHOT-sources.jar `-- txHotelDAO-3.0.2013.2-SNAPSHOT-tests.jar
Provides specific instructions for build
Usually does the right thing by default when possible
Default rules based on pom packaging type
<groupId>myorg.myproject</groupId>
<artifactId>ex1</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging> <<== determines default build
All actions of the build are directed by plugins
Even the simplest commands will result in plugin downloads when you first start
Specific plugins are wired into the build by the module packaging type
Plugins have goals (analogous to public methods)
Plugins may have default phases for the goals to be executed
Plugins can be configured
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>${java.source.version}</source>
<target>${java.target.version}</target>
</configuration>
</plugin>
<!-- surefire.argLine is set in debugger profile -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22</version>
<configuration>
<argLine>${surefire.argLine}</argLine>
</configuration>
</plugin>
...
New plugins can be added, but will need to be wired into a specific lifecycle phase of the build.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<configuration>
<argLine>${surefire.argLine}</argLine>
</configuration>
<executions>
<execution> <!-- run the tests here -->
<id>integration-tests</id>
<phase>integration-test</phase>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
<execution> <!-- delay failures to after undeploy -->
<id>verify</id>
<phase>verify</phase>
<goals>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
The above example shows the failsafe plugin being configured to share some properties with surefire and have specific goals wired into the integration-test and verify lifecycle phases.
Maven build broken into phases
Plugins are added to the build and their goals execute during a specific phase
Build invoked using the mvn
command
$ mvn (phase) (plugin:goal) -D(system-property) -P(profile)
phase
clean
Remove contents of target tree
test
Build the src/main and src/test tree and execute the unit tests as defined by the surefire plugin
pre-integration
Perform unit tests and perform any steps required to ready for integration test
integration-test
Perform integration tests as defined by the failsafe plugin
post-integration-test
Tear down anything required to perform integration tests
verify
Evaluate results of integration tests
install
Install module artifacts into local repository
(plugin:goal)
Manually trigger a plugin goal without a specific build lifecycle
ex. jetty:run, dependency:tree, or dependency:help
-D(system-property)
Manually define a system property key or key and value to be used as a property in the build
Must configure surefire/failsafe to promote these as properties Junit -- these are system properties of the maven build and not of the JVM tasks kicked off by the maven build.
-P(profile)
Manually trigger build configurations latched by a profile
e.g., which DB driver to use
-P!(profile)
Manually turn off a profile
e.g., turn off a profile activated by a rule or settings.xml
The bash shell requires the bang ("!") character to be escaped. Use -P\!(profile) when using the bash shell.
Provide groups of conditional configuration
Adds flexibility to build
Flexibility can sometimes add confusion for both developers and build tools -- be careful
Can consist of
properties
dependencies
build elements
...
Cannot include GAV elements (defining *this*
project)
<profile> <!-- defines our default database -->
<id>h2db</id>
<properties>
<jdbc.driver>org.h2.Driver</jdbc.driver>
<jdbc.url>jdbc:h2:\${basedir}/target/h2db/ejava</jdbc.url>
<jdbc.user>sa</jdbc.user>
<jdbc.password/>
<hibernate.dialect>
org.hibernate.dialect.H2Dialect
</hibernate.dialect>
</properties>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</profile>
The example above defines the properties and dependencies for the H2 database driver
when the h2db
profile is active
Use -P(profile)
to manually trigger a profile to be included
$ mvn test -Ph2db
Use -P!(profile)
to manually trigger a profile to be excluded. Use -P\!(profile) with bash shell
$ mvn test -P\!h2db
Specify an active profile in settings.xml
<activeProfiles>
<activeProfile>wildfly13</activeProfile>
<activeProfile>h2db</activeProfile>
</activeProfiles>
Property having a value
<profile>
<id>h2db</id>
<activation>
<property>
<name>jdbcdb</name>
<value>h2</value>
</property>
</activation>
The h2db profile will activate when -Djdbc=h2 is specified on the command line
Property existing
<profile>
<id>h2db</id>
<activation>
<property>
<name>h2db</name>
</property>
</activation>
The h2db profile will activate when -Dh2db is specified on the command line -- no matter the value
Property not existing
<profile>
<id>h2db</id>
<activation>
<property>
<name>!h2db</name>
</property>
</activation>
The h2db profile will activate when -Dh2db is not specified
Property not a specific value
<profile>
<id>h2db</id>
<activation>
<property>
<name>jdbcdb</name>
<value>!hsql</value>
</property>
</activation>
The h2db profile will activate in cases when -Dhsql is not equal to hsql
Maven modules boundaries are highly influenced by the re-usable artifacts that must be produced
If you have many .jars, you likely have many maven modules
If you have few but large .jars, you likely have few but large maven modules
If you require different types of artifacts, you likely have different maven modules
Maven can be used in multi-module configuration
Many maven modules share the same configuration needs -- could lead to duplication
POM Inheritance helps mitigate duplication issues
Common properties can be defined in parent pom
<project>
<groupId>info.ejava.examples</groupId>
<artifactId>examples-root</artifactId>
<version>5.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Student Root</name>
<scm>
<url>https://github.com/ejavaguy/ejava-student/tree/master</url>
<connection>scm:git:git@github.com:ejavaguy/ejava-student.git</connection>
<developerConnection>scm:git:git@github-ejavaguy:ejavaguy/ejava-student.git</developerConnection>
<tag>HEAD</tag>
</scm>
...
In the above example -- the parent pom is defining the CM repository for all sub-modules
Child projects can inherit from a parent pom
<project>
<parent>
<groupId>info.ejava.examples</groupId>
<artifactId>examples-root</artifactId>
<version>5.0.0-SNAPSHOT</version>
</parent>
<groupId>info.ejava.examples.javase</groupId>
<artifactId>javase</artifactId>
<packaging>pom</packaging>
<name>JavaSE</name>
...
In the above example, the javase project inherits the CM configuration from the parent
If you include a relativePath to the parent -- changes to parents in your build environment will be immediately reused without having to install them into the localRepository.
Use the relativePath element to specify an alternate
Use relativePath element to specify a relative path to the parent pom
<relativePath>build/dependencies/pom.xml</relativePath>
Use an empty relativePath to specify the pom in the parent directory is not the relative location of this project's pom
<relativePath/>
Specifying a relativePath to ".." is redundant information since that is the default value.
Be careful not to attempt to blindly merge all project declarations into a single parent pom. This can bloat the inheriting children with unwanted dependencies and actions, cause slower builds, and can even break a build. When in doubt -- push active declarations (like dependencies and plugin declarations) to the leaf modules and leave only passive definitions (like properties, dependencyManagement, and pluginManagement) in the parent.
Parent modules can be created to delegate build commands to a group of child modules
Child modules can be physically placed anywhere, but commonly in immediate sub-directories named after their artifactId
<project>
<parent>
<groupId>info.ejava.examples.ejb</groupId>
<artifactId>ejb</artifactId>
<version>5.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ejbsessionBank</artifactId>
<packaging>pom</packaging>
<name>EJB::Session EJB Bank Example</name>
<description>
This project is the root project for the core session bean example.
</description>
<modules>
<module>ejbsessionBankImpl</module>
<module>ejbsessionBankEJB</module>
<module>ejbsessionBankWAR</module>
<module>ejbsessionBankEAR</module>
<module>ejbsessionBankTest</module>
</modules>
</project>
The example above has several child modules that it will build in the order they are specified.
Defines what is possible without actively imposing the dependency or plugin defined
Placed in a parent of multi-module project
Consolidate management of dependency versions without adding active dependencies to all inheriting children
<properties>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
...
</dependencies>
</dependencyManagement>
Parent defines dependency versions for two artifacts (passive)
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Child declares a dependency on one of the artifacts (active)
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
Net result is a consistent version for dependency in effective pom
$ mvn help:effective-pom
Control artifact versions used in transitive dependencies
Exclude artifacts from transitive dependencies
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.jbossas</groupId>
<artifactId>jboss-as-server</artifactId>
<version>${jboss-as-server.version}</version>
<classifier>jmx-invoker-adaptor-client</classifier>
<exclusions>
<exclusion> <!-- gets in the way with JBoss6 and M2 -->
<groupId>org.jboss.security</groupId>
<artifactId>jbosssx-client</artifactId>
</exclusion>
<exclusion> <!-- gets in the way with newer versions -->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
Parent defines exclusions for possible dependency (passive)
using dependencyManagement
<dependencies>
<dependency>
<groupId>org.jboss.jbossas</groupId>
<artifactId>jboss-as-server</artifactId>
<classifier>jmx-invoker-adaptor-client</classifier>
</dependency>
Child includes simple dependency declaration (active)
using dependencies
Consolidate management of plugin versions and default configuration without adding activations to all inheriting children
<properties>
<java.source.version>1.8</java.source.version>
<java.target.version>1.8</java.target.version>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.source.version}</source>
<target>${java.target.version}</target>
</configuration>
</plugin>
...
</plugins>
</pluginManagement>
</build>
Parent defines version for compiler plugin and configures for Java 8
<parent>
<groupId>info.ejava.examples.javase</groupId>
<artifactId>javase</artifactId>
<version>5.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>ejava.javaee.intro</groupId>
<artifactId>javase5Enhancements</artifactId>
<packaging>jar</packaging>
Child of packaging=jar type automatically get compiler configured to parent specs
A more complicated example
<pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<version>${cargo-maven2-plugin.version}</version>
<configuration>
<container>
<containerId>${cargo.containerId}</containerId>
<type>remote</type>
<log>target/server.log</log>
<output>target/output.log</output>
</container>
<configuration>
<type>runtime</type>
<properties>
<cargo.hostname>${jboss.mgmt.host}</cargo.hostname>
<cargo.jboss.management.port>${jboss.mgmt.port}</cargo.jboss.management.port>
</properties>
</configuration>
</configuration>
<dependencies>
<dependency>
<groupId>org.jboss.as</groupId>
<artifactId>jboss-as-controller-client</artifactId>
<version>${jboss.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>cargo-prep</id>
<phase>pre-integration-test</phase>
<goals>
<goal>redeploy</goal>
</goals>
</execution>
<execution>
<id>cargo-post</id>
<phase>post-integration-test</phase>
<goals>
<goal>undeploy</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
Parent defines details of deployment to JBoss container (passive)
using dependencyManagement
<build>
<plugins>
<!-- artifacts to deploy to server -->
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<configuration>
<deployables>
<deployable>
<groupId>${project.groupId}</groupId>
<artifactId>ejbsessionBankEAR</artifactId>
<type>ear</type>
</deployable>
</deployables>
</configuration>
</plugin>
...
</plugins>
</build>
Child declares plugin (active) in plugins
with configuration extensions specific to child project
Maven/Eclipse integration is handled by adding the M2E plugin to your Eclipse environment and followng a pom-first configuration strategy.
Import an existing maven project into Eclipse as a maven project
M2E will inpsect the pom.xml and configure Eclipse accordingly
M2E provides mechanisms to create new maven projects (we will not discuss)
Notice the Eclipse project dependencies are in sync with the versions specified in the pom.xml
If you make a valid change to the pom.xml the Eclipse dependencies will update
Dependency hierachy shows
Which modules bring in other modules
Specific versions and where there are conflicts
When things are not automatically updated, try...
Refresh/F5
Project->Clean
Maven->Update Projects
A build from the command line that includes a clean
and fails to complete can leave Eclipse in an unhappy state
(lots of red unresolved references). Resolve with a successful
build and repeat some/all of the above actions.
Working sets can be quite helpful organizing multi-modules
You can control their contents, order them, and hide them
Maven builds can be directly launched from within Eclipse
No significant difference from normal command line
Can create custom launchers for common targets and pin for easy recall
Unit tests can be directly executed within Eclipse without Maven
Maven is quite useful in setting up robust test environments but at a cost of repeated seconds of unnecessary work at times. Have your unit tests assume reasonable defaults and provide the ability to optionally override those values through surefire or launcher configurations. That way you can develop by compiling only changes classes within Eclipse and move forward in only a few seconds. Delegate the maven build at that point to batch regression testing.
Necessary evil in application development
Powerful capabilities should not be ignored
Set program breakpoints where problems have been identified
If runnable directly from Eclipse -- use debug option
If not runnable within Eclipse -- use remote debugging option
Remote debugging requires special system properties issued to JVM
surefire and failsafe can be setup to optionally pass system properties to JUnit JVM
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<argLine>${surefire.argLine}</argLine>
<testSourceDirectory>src/test/java</testSourceDirectory>
</configuration>
</plugin>
<profile> <!-- tells surefire to run JUnit tests with remote debug -->
<id>debugger</id>
<activation>
<property>
<name>debugger</name>
</property>
</activation>
<properties>
<surefire.argLine>-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -Xnoagent -Djava.compiler=NONE</surefire.argLine>
</properties>
</profile>
Copyright © 2019 jim stafford (jim.stafford@jhu.edu)
Built on: 2019-08-22 07:09 EST
Abstract
This document contains project specifications for the eSport project. There are three projects associated with eSport; data tier and business logic, server-side deployment, and security plus asynchronous actions. Each will be part of the follow-on project.
Please follow the project submission guidelines posted in the course overview.
Table of Contents
Table of Contents
Gain experience designing and testing project implementations around layered boundaries.
Build the data access tier and initial business logic for an application.
Gain experience with database schema and Java Persistence API technologies.
Design and implement a set of business logic interfaces that address the functional requirements of the external users.
Design and implement a set of business objects to represent the data requirements in the system.
Design and implement a relational database schema that will store the state of the business objects.
Design and implement a data access tier that will handle mapping the business objects to the database using the Java Persistence API.
Design and implement the business logic classes that realize the business logic interfaces and leverage the data access objects to implement the scenario logic.
Unit test the different levels of the architecture.
Integration test the end-to-end solution according to a defined scenario.
eLeague is planning an on-line league site that will allow league coordinators to organize teams into divisions, schedule contests, and track scores. At the same time, eClub is planning an on-line club management site that will allow clubs to organize players, coaches, and managers into teams. eClub is designing their software such that it can be used with eLeague.
For eLeague, the coordinator starts by adding clubs and their primary point of contact. Venues and Teams are added to the league by club coordinators. Contacts (e.g., coach(s), manager(s), etc.) are added for teams. Prior to the start of the season, the league will create a new set of divisions and assign teams to the appropriate divisions for that season. Divisions are arranged by group (e.g., U11, Bantam, Masters, etc.) and level (e.g., AA, A, 1, 2, etc.). The league will then schedule contests for each division, matching a home and away team with a venue from the home team's club. For simplicity, we'll assume that the venue is exclusively available for scheduling by the league, but the league still must manage scheduling conflicts for the venue contests within the league. Scores are added for the home and away team after each contest. A team schedule and division standings will be generated from the information.
For eClub, individuals register with the site, supplying basic information. For simplicity, we'll keep eClub to minimal information. The individuals can be players or parents and/or coaches. There is additional information tracked for players and coaches. Players will need a position and jersey number. Coaches need a certification number. Parents/guardians will need no further information. After tryouts, the club will create a set of teams and assign a head coach, a manager, and players. The team and team points of contact will be used to form a registration with eLeague. From the club site, a team can get roster information, game schedules, and division standings. The schedules and division standings are obtained from eLeague and and are not duplicated within eClub to make sure they are up to date.
Both eLeague and eClub have come to you to develop the initial phase of their applications. You are tasked with implementing a low-cost prototype, based on current standards, to automated much of this activity. At this point in the project we are primarily looking to build the data access tiers for both eLeague and eClub (two separate systems). We will also add a minor amount of business logic to coordinate the data access between the individual data access objects.
For eLeague, the system is initialized with a starting set of information pertaining to clubs (league's external view), contacts, venues, and a season defined with divisions and teams. New information can be added and updated.
For eClub, the system is initialized with an empty database that is designed to hold internal club information for teams, coaches, and players. The teams will be assigned a division within the league and individuals associated with the team will be interested in seeing scores and standings relative to their team.
The work done during this project focuses on the business objects (BOs), the data access objects (DAOs) of the data access tier, some initial business logic interfaces (BL), and business logic implementations (BLImpl). The DAOs will be based on the Java Persistence API (JPA). How you partition the implementations of your projects is up to you. A candidate starting point is provided at the end of this project description.
There will be many choices while creating your solution to the two applications. It is intended that you always approach the eLeague application as "it is hard" and requires extra formal layers. To provide contrast, it is intended that you always approach the eClub application as "it is simple" and target as many simplification and shortcuts in the architectural layers as you can find. The point of this is for you to gain experience with the different approaches and to not make eClub a tedious clone of approaches taken in eLeague. The spec will describe both in a similar manner, but you should apply more of a "right approach" based on what was just stated.
The business objects encapsulate the core business data and rules of the application. You will design the business objects and then map them to the database. You are required to implement CRUD (Create, Read, Update, and Delete) capability with JPA but you only need to implement the CRUD methods that are necessary to complete the provided end-to-end scenario. You will be required to encapsulate all object to relational (O/R) mapping within the DAOs, descriptor files, and/or class metadata annotations. You will be given a set of test data to initially populate your applications and be the source of data for the ingest requirement. To use the data, you will ingest using a parser supplied by the instructor. There is a sample thread in the projects/eSport/eLeague directory that shows how to use the parser as well as other aspects technology within the project.
The database will have a functional schema and indexes to provide query performance. Business objects will be validated in the database using schema constraints and within the JVM using the Validation API.
The business logic will provide a set of classes with concise methods that map easily to the provided end-to-end scenario. The business logic will ensure proper use of the overall application, delegating some business logic functionality to the business objects and full O/R responsibility to the DAOs. eLeague will have an ingest requirement as well as the requirement to manipulate and add to what was ingested. eClub will start fresh and obtain all data from users and coordinate with eLeague. However, for project 1, eClub will be unable to fully implement data exchange with eLeague because remote interfaces will not be implemented until project2. Some of that must be stubbed at this point. You are only required to implement enough business logic methods that it takes to implement the end-to-end scenarios specified later within this specification.
The test acceptance for the first project will be the unit tests and an integration test that takes the business logic, data access tier, and business objects through a provided end-to-end scenario that will be consistent during the semester. You are required to supply the following tests:
A unit test that implements the steps of the provided eLeague end-to-end scenario
A unit test that implements the steps of the provided eClub end-to-end scenario
At least one unit test per architectural layer (BO, DAO, BL) that demonstrates your ability to test at that level.
The amount of work that you can implement within the bounds of this project spec could be endless if you attempted to account for everything and anything. It is very important that you limit your work to only what is necessary to implement (and test) the functionality required for the end-to-end scenario. That means you may have entities that are designed to be created but never modified while having other entities that go through a full lifecycle. When deciding to add or skip certain capabilities -- always ask yourself "does the business logic need this behavior to implement the end-to-end scenario".
Since the work is for separate applications, we will need to establish two separate application projects for this work; eLeague and eClub. The development can physically share resources (e.g., same database and application server), but should be easily separated. It is suggested that you form a root for the overall eSport work to coordinate integration testing, and then group the lower-level work under two mid-level projects; eLeague and eClub. See some suggested project layouts at the bottom of this specification. A sample set of projects that implement a thin eLeague thread has been made available. Please ignore the sibling eSportData project when using the example to craft your source tree. You will depend on projects within the sibling data tree -- not re-implement them and not copy them. See the Getting Started section towards the end of this specification for a more detailed sample project layout.
You will likely copy significant portions of the thin thread example and other class examples into your project. Be aware that the thin thread and other example pom.xml files inherit from the class root project that provide dependencyManagement and pluginManagement duties. You will either need to also inherit from the course dependencies/pom.xml or compensate by re-defining the management sections in the root pom.xml of your project.
Since students in the class will be producing parallel implementations for the applications and submitting them for evaluation, it is asked that you come up with unique names for your artifacts. This could be done by replacing the "e" in eSport, eLeague, and eClub with a unique name that corresponds to your newsgroup or college login (ex. jcsMarket, jcsSales, jcsBidbot). You should also use this same pattern for java packages (ex. jcs.sales, jcs.bitbot), DB tables (e.g., JCS_), etc.
There should be no use of System.out.println() in the code and all implementations must use a logging API with log4j as the logging provider. You may leave debug in your code, but this should be able to be turned on/off with the proper logging priority changes in the log4j.xml configuration.
This project provides an obvious opportunity to use compound business primary keys and the documentation below depicts some of the ramifications in doing so. Synthetic, single value primary keys are permitted in your solution without penalty. Single value primary keys are much easier and should be the first choice if you have limited experience and limited time to work with the more complex primary key and foreign key mechanism.
Design 2 sets of database schema that account for the following information. Although we will deploy the 2 database schemas to the same database for the project, they should be designed to be independently deployed to separate databases. eLeague and eClub are two independent applications. This will primarily affect your attempt re-use tables or to make primary key assumptions between the two.
This table is used to store singleton information for the entire application. Although JPA requires a primary key field, this table should have only one row. Right now, it is being depicted as referencing the current season.
LEAGUE_NAME
Unique name for the league that will not change. It can be used as a primary key.
SEASON_ID
(1 to 0..1 relation to ELEAGUE_SEASON) - this references the most current season and will be NULL before the start of the first season.
COORDINATOR_ID
(1 to 0..1 relation to the ELEAGUE_CONTACT) - designates the overall coordinator for the league. They will be responsible for adding clubs, forming divisions, and scheduling contests.
Design a table that contains POC information for individuals responsible for the league, divisions, and teams. This information is primarily on-line information and need only contain a textual name and an e-mail address. There is no need for any more detailed information and we don't want to expose phone numbers.
ID
This is a synthetic/generated id to identify the contact.
CONTACT_NAME
This is a required, non-unique textual name for the individual. This will be posted the league site and can be updated.
EMAIL
This contains the e-mail contact information for the individual that will be posted to the web site. It its highly recommended for optimal operation of the league, but may not be available when contacts are initially entered. We should not treat this field as unique since family members have been known to share the same league e-mail address.
LOGIN
This is a field that can be used to represent the individual's login to the web site. This field is not required. However, if it is not supplied, it is assumed that they will not be personally accessing the site. This field can be updated, thus it would not work well as a primary key.
Design a table to store the name and core information associated with a season. Seasons will never be removed as a part of normal operation.
ID
The unique ID for the season. This value is required, must be unique, and cannot be changed. It can be used as a primary key if desired. It can be a natural value (e.g., Fall2000) or a synthetic value.
SEASON_NAME
The textual name for the season (e.g., "Fall 2000"). This value is required and must be unique. It could be used as a primary key if desired, but is thought to be more of a textual value that may require modification.
START_DATE
END_DATE
LEAGUE_ID
(N:1 relation to ELEAGUE_LEAGUE). This relationship allows all seasons to form a DB relationship to the league.
Design a table that represents a division for teams, for a single season. Divisions will be arranged by group (i.e., age) and level of play. Since group/level names cannot be relied on for any specific ordering, you may wish to add a competitive rank property (11, 21, 22, 23 might equate to U11-D1, U12-D1, U12-D2, U12-D3 with U11 and U12 being a group and D1, D2, and D3 being a level).
SEASON_ID
(N:1 relation to ELEAGUE_SEASON)
GROUP
A name for a class of teams, such as an age group (e.g., U11, Bantam, or Masters). This is required and does not change.
LEVEL
A name for the competitive level of play (e.g., DI, DII -or- AA, A, B, etc.)
COORDINATOR_ID
(N:1 relation to ELEAGUE_CONTACT) - this individual will be responsible for updating scores for contests within the division.
COMP_RANKING
A value that can be used to sort divisions by their competitive ranking rather than an ASCII sort of their group and level names.
Design a table that represents a team's participation in a division for a season.
TEAM_ID
(N:1 relation to ELEAGUE_TEAM) - a team will participate in 0 to many seasons and we will keep historical information of the teams from season to season.
SEASON_ID,GROUP,LEVEL
* (N:1 relation to ELEAGUE_DIVISION) - a team will participate in only one division per season.
Design a table that represents a team over time. We are interested in tracking points of contact for the team and divisional play. We do want to keep current and historical division play, but do not need to keep historical contact information.
ID
An artificial id for the team
TEAM_NAME
Each team must have a name, but it need not be unique
CLUB_ID
Each team must have a name, but it need not be unique
Design a table that describes the role played by a contact for a team. Contacts are usually a manager or coach. You can have multiple contacts for a specific team and they can even have the same role (e.g., someone can be coach and manager and two people can be a (assistant?) coach). However, no one can have multiple roles of the name for a single team (e.g., you can be manger for 2 teams, but you can't manage the same team twice). Therefore we have the option of forming a compound primary key out of the role name, contact foreign key, and team foreign key.
ROLE_NAME
Contains the type of role the contact plays for a team (e.g., manager, coach). This value is required and will not change.
CONTACT_ID
(N:1 to ELEAGUE_CONTACT). This reference is mandatory and will not change.
TEAM_ID
(N:1 to ELEAGUE_TEAM). This reference is mandatory and will not change.
Design a table that represents a club for teams.
ID
An artificial primary key value
CLUB_NAME
The textual name for the club. This value must be supplied, may be changed, and must be unique.
REP_ID
(N:1 to ELEAGUE_CONTACT). This reference supplies the contact information for the club rep.
Design a field that represents a venue for holding contests. Venues are owned by a club, but are given to the league to control their scheduling. Other than scheduling, the key information is to provide map, direction, and other information that would be helpful for visiting teams to locate the venue.
ID
This is an artificial value to identify a venue.
CLUB_ID
(N:1 to ELEAGUE_CLUB)
VENUE_NAME
A textual name used to identify a venue (e.g., YaYa Park, field #3)
ADC_PAGE
This is a text field containing the ADC map, page, and grid where the venue is located.
DIRECTIONS
This is provided by the club and may be updated to reflect current direction information. Some clubs even add information about whether pets are allowed or whether there are bathrooms are available on-site.
ADDRESS_ID
(N:1 to ELEAGUE_ADDRESS) - this is required.
Design a table to hold an address. Although it could be used for anything, it will primarily be used for people attempting to locate a venue by address information using Google Maps or their GPS Navigation.
ID
This is an artificial value to identify a specific address
STREET1
STREET2
CITY
STATE
ZIP
Design a table to hold contest information. Contents are scheduled at venues for a home and away team. They are for a specific date, start and end time, and should never overlap with another contest for the same venue. Contests are always for teams in the same division.
ID
This is an artificial value to identify a specific contest.
START_TIME
Identifies the time of day the contest will start. This field may not be known when the contest is first identified or post-poned, so it is not required.
DURATION or END_TIME
This either can identify the amount of time allocated to the contest or express the actual end time.
VENUE_ID
(N:0..1 relation to ELEAGUE_VENUE) This relationship may not be known when the contest is first identified or post-poned, so it is not required.
HOME_TEAM_ID
(N:1 relation to ELEAGUE_TEAM_SEASON) This relationship is required. Note that it is related to the team's season in this database and not directly to the team's table in eClub.
AWAY_TEAM_ID
(N:1 relation to ELEAGUE_TEAM_SEASON) This relationship is required. Note that it is related to the team's season in this database and not directly to the team's table in eClub.
SEASON_ID, GROUP, LEVEL
Used to complete the compound foreign key values to the home and away teams. Since teams must be in the same division, we should be able to reuse these columns to enforce that rule.
HOME_SCORE
Score for home team to be updated by the division coordinator.
AWAY_SCORE
Score for away team to be updated by the division coordinator.
Design a table to contain information associated with an individual in the club.
ID
This is an artificial value used to identify an individual.
FIRST_NAME
This should be required, but may be updated
LAST_NAME
This should be required, but may be updated
EMAIL
This is optional
LOGIN
This is optional. Users without a login cannot login to update their information.
Design a table to hold a many-to-many link table representing parent/child relationships between individuals in the club.
CHILD_ID
(N:1 relation to ECLUB_INDIVIDUAL)
PARENT_ID
(N:1 relation to ECLUB_INDIVIDUAL)
Design a table to hold information specific to a player.
ID
(1:1 relation to ECLUB_INDIVIDUAL). This is both a primary and foreign key value.
JERSEY_NO
This is optional and can be updated over time.
POSITION
This is optional and can be updated over time.
DOB
This is the date of birth for the player used to help assign a player to a pool of teams. It is required, but may need correcting over time.
Design a table to hold information specific to a coach.
ID
(1:1 relation to ECLUB_INDIVIDUAL). This is both a primary and foreign key value.
CERT_NO
This is optional and can be updated. It will contain their coaching certification number.
CERT_LEVEL
This is optional and can be updated. It will have values like 1, 2, 3, etc.
Design a table to hold information specific to a team within the club. Note that historical information for teams is not kept from season to season. For simplicity, we will only assign a single head coach (no assistant coaches). Note also the information below is internal to eClub. You may want to include an indentifer the league tracks this team by and store it here for querying the league about the team.
ID
This is an artificial value used to refer to a specific team.
TEAM_NAME
This is a required field used to hold a textual name for the team. It can be updated.
LEAGUE
This is an optional field that can be updated over time. It will identify which league they are playing in.
GROUP
This is an optional field that can be updated over time. It will identify which grouping the team is playing within a league (e.g., U11 or Bantam).
LEVEL
MANAGER_ID
(N:0..1 relation to ECLUB_INDIVIDUAL). This is optional and can be updated over time.
HEAD_COACH_ID
(N:0..1 relation to ECLUB_COACH). This is optional and can be updated over time.
LEAGUE_TEAM_ID
This is an optional field that will get updated once the team is registered with the league. Since the League and Club databases are separate, we cannot re-use IDs between them.
Design a table to hold the many-to-one linkage between players and teams. Teams have multiple players and we'll limit players to only one team so far. If we model this as a link table, it will be easier later on to allow a player to be on multiple teams.
PLAYER_ID
(N:1 relation to ECLUB_PLAYER)
TEAM_ID
(N:1 relation to ECLUB_TEAM)
I discovered very late in the preparation for the Fall 2018 course that the course dependencies are set to Hibernate 5.3.x, to leverage JPA 2.2 in some of the examples and capabilities. However, without being and to fully run Wildfly in ee8 preview mode, we will be limited to Hibernate 5.1.x when deployed to the server in follow-on projects this semester. That version of Hibernate is JPA 2.1. This should not be an issue unless you make use of the new Java DateTime API mappings or the getStream() query result.
Design a class that encapsulates the points of contact in the system. This information will primarily be used to post point of contact information for teams, divisions, clubs, and the league to the web site.
id:int
name:String
Textual name for individual to be known as.
email:String
login:String
May be assigned for individuals to login and be able to modify team, division, and contest information that they are responsible for.
Design a class that can be used to wrap a Contact for points of contact where their role must be qualified. This is the case for team points of contact where the individual can be one of manager or coach.
id:int
roleName
Enum(UNKNOWN, MANAGER, COACH)
contact:Contact
team:Team
Design a class that holds information for a venue. It is shown as being as being made up of an embedded Address class, where the address class holds no primary key of its own. You may map the 2 table in alternate ways.
id:int
name:String
directions:String
adcPage:String
address:Address
club:Club
Design a class that represents a location that can be driven to. This is being depicted as an embedded class, with no primary key value of its own. You may map it in other ways.
street1:String
street2:String
city:String
state:String
zip:String
Design a class that represents a Club and all of its venues and teams.
id:int
name:String
teams:Collection<Team>
venues:Collection<Team>
Design a class that represents information for a team.
id:int
name:String
club:Club
contacts:Collection<ContactRole>
Design a class that represents a team's division play within a season. It is anticipated that this class will have a compound primary key with the identifying information for the team and division. You will need to create an embeddable primary key to hold the information.
team:Team
division:Division
contests:List<Contest>
A list of contests, possibly ordered by date.
wins:int
A transient, read-only property calculated by contests the team has won this season.
losses:int
A transient, read-only property calculated by contests the team has lost this season.
ties:int
A transient, read-only property calculated by contests the team has tied this season.
Design a class that represents divisional play between teams within a season. Since group/level names cannot be relied on for any specific ordering, you may wish to add a competitive rank property (11, 21, 22, 23 might equate to U11-D1, U12-D1, U12-D2, U12-D3). This class also may have a compound primary key (again, these are technical details you can decide).
id:int
season:Season
name:String
(has been referred to as "group" at times)
level:int
Used to determine competitive level. May have been referred to as "ranking" at times.
coordinator:Contact
teams:List<TeamSeason>
Design a class that represents play for a perion of time when contests will be held.
id:int
name:String
startDate:Date
endDate:Date
Design a class that represents a contest at a venue for a home and away team. Note that the start/end times or date of contest information may or may not map directly to database column. Some of these values may be derived from other values and therefore are "Transient" as far as O/R Mapping concerned. I will list the properties that callers of this class will be concerned about; whether they come directly from the database or are derived.
id:int
startTime:Date
Callers will want something they can translate to a user output that represents hh:mm
endTime:Date
Callers will want something they can translate to a user output that represents hh:mm. Schedulers will also need this to determine conflict.
location:Venue
homeTeam:TeamSeason
awayTeam:TeamSeason
isComplete():boolean
Another field or null or negative scores can indicate a contest that has not yet completed.
homeScore:int or Integer
awayScore:int or Integer
Design a class that encapsulates information for an individual in the club.
id:int
firstName:String
lastName:String
email:String
login:String
parents:List<Individual>
Zero or more parents
children:List<Individual>
Zero or more children
Design a class that encapsulates information for a player.
jerseyNo:Integer
Jersey numbers are not assigned right away.
position:String
Position is not assigned right away and can be changed.
dob:Date
Player's date of birth.
individual:Individual
The player's identity
team:ClubTeam
Keep players to a single team for simplicity.
Design a class that encapsulates information for a coach.
certLevel:String
certNo:String
individual:Individual
Coach's identity.
team:ClubTeam
Keep coach to a single team for simplicity.
Design a class that encapsulates information for a team. You can call it "Team", but there is already a class called "Team" in eLeague and may get confusing.
id:int
name:String
league:String
Identifier for league participating with. Not used.
levelName:String
Descriptive name of league's division level. Not used.
level:Integer
Competitive level team wishes to participate at or is participating at. Not used.
leagueTeamId:Integer
A key value that identifies the team within the eLeague system. ** This is used **
manager:Individual
headCoach:Coach
players:List<Player>
Add validation API declarations to your business objects as appropriate. This need not be extensive or exhaustive. Just do enough to show use of declarative validation as a part of your application.
Determine the validation groups you wish to use in your application (suggest business logic interface and persistence tier).
Add validation annotations to your business objects and assign them to your designed groups as appropriate.
Manually invoke validation from the your junit test and integrate the validation into your persistence unit.
Design and implement a mechanism to ingest a starting state for eLeague based on a provided data file and parser. You will implement two primary sets of classes to support this requirement; the DAO(s) and an Ingestor.
Design and implement a set of DAOs that can be used to ingest eLeague business data into the database using the database schema you designed as a part of a separate requirement. These DAOs can optionally be tuned for ingest or simply reused from your CRUD-style requirements.
Design and implement an Ingestor that will use an externally provided parser to obtain business data for eLeague and use the Ingest DAO to populate the database.
Please ignore references in the diagrams that call out use of JDBC. All DAOs can be implemented exclusively with JPA for this assignment.
Design and implement a DAO layer that will map the business objects between the object model and the database using the Java Persistence API (JPA). These DAOs will support all standard CRUD operations and can optionally implement the same interface as other potential DAO implementations. The implementation can make liberal use of JPA @Annotations, descriptor files, or a combination of both. Your interface should encapsulate the fact that an EntityManager is being used and the same EntityManager should be shared among other DAOs in the same Thread. Your DAOs should not attempt to control the transaction or they will NOT be portable to the EJB tier.
LeagueDAO/JPALeagueDAO
Encapsulates the use of JPA when mapping League and closely associated business objects (Season, Division, TeamSeason, Contest, and Contact) to/from the database.
ClubDAO/JPAClubDAO
Encapsulates the use of JPA when mapping Club and closely associated business objects (Venue, Address, Team, and Contact) to/from the database.
Add tuning to your database schema by augmenting the DDL files with indexes for foreign keys, joins, and where clauses. This need not be extensive or exhaustive. Just do enough to show that proper data model and database tuning is part of the overall enterprise development process.
Design an initial business interface and business logic for the applications. The core O/R mapping work will be done by the DAOs. However, it is the ultimate responsibility of these business logic implementations that either it or the business objects enforce the business rules of the application. The DAOs only perform O/R mapping and do not enforce such things as business ordering. The business logic is assumed to work within the context of a single, externally controlled transaction. Do not attempt to control the transaction of the EntityManager within these objects or you will NOT be portable to the EJB tier (without extra work). You need only implement the behavior required to implement the end-to-end use case listed in the testing section. Some of the anticipated methods are listed below.
LeagueMgmt/LeagueMgmtImpl
Encapsulate the actions required to create Clubs and manage Seasons and Divisions.
addSeason - league coordinators will need to create a new season for the league. The league cannot have 2 or more concurrent seasons and will know the most current season, if any.
addDivision - league coordinators will need to add divisions for a season. Club coordinators will be assigning their teams to the league's divisions for a season.
getDivisions - users will need to know which divisions exist for the most current season. There will also need to be the capability to get historical information for previous seasons.
createClub - league coordinators will need to create new clubs for the league. Once the club has been created, ClubMgmt can be used to perform management of the club details.
assignTeamDivision - clubs will have to assign their teams to divisions after the league has created them for an upcoming season.
getDivisionStandings - users will want to get divisional standings information for the most current season. This would normally consist of an ordered list of teams based on wins (you can ignore ties) and their win, loss, tie totals. Note that this may involve the creation of a transient object that is calculated on-demand by business objects and may not be persisted in the database. There will also need to be the capability to get historical information for previous seasons.
getTeam - regular users and team/club officials will need to obtain team points of contact, schedule, and contest results for the most current season. There will also need to be the capability to get historical information for previous seasons.
ClubMgmt/ClubMgmtImpl
Encapsulate the actions required to manage Venues and Teams.
addVenue - you will need to create one or more venues for the league to schedule contests for your club. The only thing that the club manages for the venue is keeping directional information up to date.
addTeam - you will need to create teams that play for the club. Note that clubs span multiple seasons/divisions and may even sit out a season. Therefore, teams are usually added first and then later assigned to divisions.
updateTeamContact - clubs and teams will need to update points of contact information.
ContestMgmt/ContestMgmtImpl
Encapsulate the actions required to schedule and manage contests between teams within the league.
scheduleSeason - league officials will need to create a home and away schedule for each team in the division with each team playing each other at least once and possibly more (for smaller divisions), up to a specified number (default to 10) number of games. Your scheduling can be extremely simple as long as it does not schedule conflicting contests. Ideally a team would not play more than once on a single day and you might want to limit contests to a specific set of days of the week. However, the fact that you have placed the scheduling within the correct architectural area is the key point. How simple you make the algorithm is totally up to you and will not impact your grade. You may decide how much scheduling gets done by the business object(s) and how much gets done by the business logic.
reportScore - division coordinators will need to be able to report the results of a contest.
LeagueTestUtil/LeagueTestUtilImpl
A useful tool during testing that encapsulates how to get the application back into a known state prior to running a test or to inspect values not normally exposed through the normal business interfaces.
resetAll - sanely take the state of the system down to a coldstart.
populate - you might want the database populated with a known state prior to running a test. This may delegate to the LeagueIngestor.
get/doXXX - methods that are unsafe for the actual business logic, but are needed for development and test.
LeagueIngestor
The Ingestor written as a part of a separate requirement is also logically considered part of this tier.
ingest - point an externally provided parser at a set of test data and use the DAOs to populate the system to a known state.
Keep in mind that this project has two notions of a club. From the league's perpective it manages schedules, scores, and high level contact information for teams within a club. From the club's perspective, there is the need to manage coaches, players, and parents associated with the team. For that reason -- I am calling the club an organization here to avoid some confusion.
MemberMgmt/MemberMgmtImpl
Encapsulates the actions required to manage individuals registering with the club.
createParent - parents will need to be able to register with the club.
createPlayer - parents will need the ability to register their minors as players. All players will need at least one parent.
addCoachRole - coaches will need to be able to register with the club. It is common that a coach is also a parent.
getIndividual, Player, and Coach - created individuals will need to be retrieved.
OrgMgmt/OrgMgmtImpl
Encapsulate the actions required to create teams and get team information. Some of this information will come from contacting eLeague in future projects.
createTeam - club officials will need to create teams.
assignPlayers - club officials will need to assign players to teams
assignCoach - club officials will need the ability to assign a coach to a team
assignManager - club officials will need to ability to assign a manager to a team
getTeamRoster - users assigned to a team will need the ability to get the roster for team. Rosters contain coach, manager, player, and parent information.
getTeamSchedule - users will need the ability to get the schedule for a team for the most current season. This information will come from eLeague in a future project. Team schedules should have contest dates, venue, and scores.
ClubTestUtil/ClubTestUtilImpl
A useful tool during testing that encapsulates how to get the application back into a known state prior to running a test or to inspect values not normally exposed through the normal business interfaces.
resetAll - sanely take the state of the system down to a coldstart.
populate - it may be helpful to return the database to a known populated state between tests.
get/doXXX - methods that are unsafe for the actual business logic, but are needed for development and test.
The following sketch of two directory structures can be used as a starting point for your overall application. The first is a simplified project layout that collapses the number of sub-projects into a single "Impl" project. It is suggested that you use this layout if you are new to maven and want the simplest configuration possible. The second is a more robust layout and is closer to a multi-developer environment. Use the later structure if you want to better simulate a work environment where the work of multiple developers needs clearer separation.
All "e"Sport, "e"League, and "e"Club names should be changed to your specific name mangler. The same type of modification needs to occur for the java package names.
Other than a README, it is not anticipated that you will have other artifacts at the root layer. The root pom.xml should be used only as a convenience wrapper to perform goals across both projects. The root project can also be used for common property, dependencyManagement, and pluginManagement defintions. All concrete dependency and plugins should be defined in the leaf-level poms to avoid unwanted dependencies from the root or mid-level parents.
Figure 18.1. Single Module Approach Candidate Module Structure
eSport |-- eLeague | +--eLeagueImpl | | |-- pom.xml | | `-- src | | |-- main | | | |-- java | | | | `-- eleague | | | | |-- bo | | | | |-- dao | | | | |-- jpa | | | | |-- bl | | | | |-- blimpl | | | `-- resources | | | `-- ddl | | | |-- eLeague-create.ddl | | | |-- eLeague-drop.ddl | | | `-- (eLeague-tuning.ddl or within create) | | `-- test | | |-- java | | | `-- eleague | | | |-- bo | | | |-- dao | | | `-- bl | | `-- resources | | |-- log4j.xml | | `-- META-INF | | `-- persistence.xml | `-- pom.xml |-- eClub | +--eClubImpl | | |-- pom.xml | | `-- src | | |-- main | | | |-- java | | | | `-- eclub | | | | |-- bo | | | | |-- dao | | | | |-- bl | | | `-- resources | | | `-- ddl | | | |-- eClub-create.ddl | | | |-- eClub-drop.ddl | | | `-- (eClub-tuning.ddl or within create) | | `-- test | | |-- java | | | `-- eclub | | | |-- bo | | | |-- dao | | | |-- bl | | `-- resources | | |-- log4j.xml | | `-- META-INF | | `-- persistence.xml | `-- pom.xml `-- pom.xml
Figure 18.2. Multi-Module Approach Candidate Module Structure
eLeague |-- eLeague | +--eLeagueBO | | |-- pom.xml | | `-- src | | |-- main | | | `-- java | | | `-- eleague | | | |-- bo | | | `-- bl | | `-- test | | |-- java | | | `-- eleague | | | `-- bo | | `-- resources | | `log4j.xml | +--eLeagueDAO | | |-- pom.xml | | `-- src | | |-- main | | | |-- java | | | | `-- eleague | | | | |-- dao | | | | `-- jpa | | | `-- resources | | | `-- ddl | | | |-- eLeague-create.ddl | | | `-- eLeague-drop.ddl | | | `-- (eLeague-tuning.ddl or within create) | | `-- test | | |-- java | | | `-- eleague | | | `-- dao | | `-- resources | | |-- log4j.xml | | `-- META-INF | | `-- persistence.xml | +--eLeagueBLImpl | | |-- pom.xml | | `-- src | | |-- main | | | `-- java | | | `-- eleague | | | `-- blimpl | | `-- test | | |-- java | | | `-- eleague | | | `-- bl | | `-- resources | | `-- log4j.xml | `-- pom.xml |-- eClub | ("it is simple" keep to a single module as shown above) `-- pom.xml
Since the work of project 1 will be deployed to the application server in follow-on projects, the persistence.xml definitions created in project 1 should be considered strictly for test and should be placed in the "src/test" tree to prevent it from being deployed to the application server. It is also suggested that if your persistence unit is called "X", the persistence unit name for project 1 in the "src/test" tree be called "X-test" to avoid confusion of what is intended to be used.
The end-to-end scenario will test your business logic, DAO, and BOs in a "happy path" scenario. Any white-box or black-box testing of alternate and error paths would be appropriate to put in the separate unit tests.
Provide a JUnit test for your business objects (BOs) that test the manipulation of data. An example test might be to try forming a contest between teams in separate divisions or schedule a contest that conflicts with a team and/or venue schedule. These tests should be packaged with the BOs. There should be a separate project and test for both eLeague and eClub. It is anticipated that these tests will be a minimal demonstration of understanding.
Provide a JUnit test for your eLeague and eClub JPA DAOs. This should test the implementation for the required CRUD operations for each type of object. It is understood that some of the operations will be handled by cascades, so you might not have a set of methods handling each type of business object. This test should be packaged with the DAOs.
Provide a JUnit test for your business logic to test the basic functionality of your business logic design, including ingest. The ingestor test should be able to reference a known data file and ingest records into the database using the DAOs. These tests should be packaged with the business logic implementation.
Provide a set of JUnit test programs to verify the following end-to-end scenario in eLeague. This test should be implemented as a JUnit test and packaged with the business logic implementation.
reset (using LeagueTestUtil) -- reset the eLeague database to an initial starting state.
ingest data (using LeagueIngestor) -- ingest the full XML test data file.
createClub (using LeagueMgmtImpl)
addVenue (using ClubMgmtImpl)
addTeam (using ClubMgmtImpl)
updateContact for team (using ClubMgmtImpl)
assignTeamDivision (using LeagueMgmtImpl) - divisions already exist for current/next season within the test data.
scheduleSeason (using ContestMgmtImpl)
getDivisions (using LeagueMgmtImpl)
getDivisionStandings (using LeagueMgmtImpl)
getTeam with schedule and results (using LeagueMgmtImpl)
reportScore (using ContestMgmtImpl)
getTeam with schedule and results (using LeagueMgmtImpl)
getDivisionStandings (using LeagueMgmtImpl)
Provide a set of JUnit test programs to verify the following end-to-end scenario in eClub. This test should be implemented as a JUnit test and packaged with the business logic implementation. Note that it is anticipated that you may only have time to create one team, with one player, manager, parent, and coach for the club.
reset (using OrganizationTestUtil) -- reset the organization database to an initial starting state.
createParent (MemberMgmtImpl) - parent depicted as Individual
createPlayer (MemberMgmtImpl)
createCoach (MemberMgmtImpl)
createTeam (using OrgMgmtImpl)
assignPlayers (using OrgMgmtImpl)
assignCoach (using OrgMgmtImpl)
assignManager (using OrgMgmtImpl)
getTeamRoster (using OrgMgmtImpl)
getTeamSchedule (using OrgMgmtImpl) - this will later call eLeague for Contest information, but for now simply returns an empty list.
Your project will be graded on completeness and quality of product. In order for you to receive full credit in each area, it must be a) complete, b) done well, and c) tested. The breakdown of grading will be as follows:
README provided that describes where each requirement satisfied: 10pts
Projects cleanly builds with Maven: 15pts
Managed schema to include declaration of key indexes (i.e., defined in a set of files and explicitly used to coldstart and initialize the database at defined times): 5pts
Business Objects: 10pts
Use of Validation API: 5pts
JPA DAO and JPA O/R Mapping: 25pts
Ingest: 10pts
Business Logic: 10pts
End-to-end Integration Test: 10pts
The following table contains examples of where projects have lost points in the past. Of course, each project submitted can introduce new issues or different severity levels of the same issues. Do not treat this as a complete list.
Table 20.1. Sample Lost Points
README | |||
---|---|---|---|
Not provided | 10 |
Projects cleanly builds with Maven | |||
---|---|---|---|
groupIds, schema, java packaging, etc do not have a project-specific name mangler | 2 | ||
Avoidable build errors | 2 | ||
Key areas not building | 10 | ||
Poluting project with do-nothing tests or tests that are not tests | 2 | ||
Managing multiple copies of the same source file. | 1 | ||
Large commented out blocks of code. | 2-5 | ||
Non-portable references to external resources (e.g., ingested file) | 2 |
Managed Schema | |||
---|---|---|---|
Database columns not well defined and constrained (e.g., FKs, non-null, max size) | 1 | ||
Improper use of DATE, TIME, and/or TIMESTAMP | 1 | ||
No definition of any indexes | 2 |
Business Objects | |||
---|---|---|---|
Improper/no TemporalType declaration for Dates | 1 | ||
PK classes (if exist) did not implement required constructs | 1 |
Use of Validation API | |||
---|---|---|---|
Missing any declaration of validation criteria in BOs | 3 | ||
Not explicitly performing validation | 2 |
JPA DAO and JPA O/R Mapping | |||
---|---|---|---|
Missing key relationships | 5 | ||
Managing transactions in DAO | 1 | ||
Relationships should not be modeled as entities | 2 | ||
Using provider-specific mechanisms over JPA-provided technique | 1 | ||
Not honoring dependencies. Attempt to delete entities with incoming relationships/FKs. | 1 | ||
Walking the object tree functionally works but using a JPA-QL query would be much cleaner and more efficient. | 2 |
Ingest | |||
---|---|---|---|
Re-used IDs from ingested XML file and did not generate IDs local to project | 2 | ||
Did not include a unit test that *verified* injest worked | 1 |
Business Logic | |||
---|---|---|---|
Implemented stateful logic (Wrong!) | 5 | ||
No separate testing of business logic. Relied too much on end-to-end as unit test. | 1 | ||
Managing transactions in business logic | 1 | ||
Did not implement all methods required for end-to-end | 1 | ||
Not exhibiting good command of what it means for an entity to be managed and what you do and don't need to do in that state | 1 | ||
Persisting BOs from other application. Not preserving separation between applications. | 1 |
End-to-end Integration Test | |||
---|---|---|---|
Poluted, hard to follow, too much extra stuff | 2 | ||
Missing resetAll and populate at start of scenarios | 5 | ||
Missing step X | 1 |
Table of Contents
Continue to re-use and update the implementations from the previously implemented data access tier and initial business logic
Deploy data access tier and business logic as an EJB component to server
Configure server-based components
Deploy EJBs using different deployment approaches (EAR and WAR)
Provide a remote interfaces to business logic using RMI and JAX-RS
Define transaction boundaries using the EJB container
Provide integration prototype for Web UI
Expand the current project development architecture to include EJB, WAR, and EAR components and remote Tests for eLeague. For eLeague you will have separate EJB, WAR, EAR, and remote Test modules to host the new components and test. This is being done to simulate a complex project option.
Expand the current project development architecture to include EJB and WAR components and remote Tests for eClub. There will be a few Maven module options for you to take, but in the end all server-side logic for eClub must be deployed to the server in a single WAR artifact. This is being done to simulate a simple project option.
Create and configure the EJB and WEB components with necessary persistence context (EntityManager), component, and EJB injections.
Deploy the server-side components as part of testing.
Re-host the data access tiers and initial business logic from Project 1 within the new EJB and WEB components deployed to the server-side.
Design and implement transactional behavior of business methods using explicit EJB annotations.
Design and implement remote application interfaces (APIs) to server-side components using RMI (some) and JAX-RS (mostly).
Design and implement data transfer objects (DTOs) to be used to communicate with clients using the remote interfaces. For eLeague you will design a set of DTO classes separate from your BO classes. For eClub you will reuse your BO classes as DTOs.
Create remote tests that verify functionality and compliance with requirements using remote EJB interfaces.
Create a browser-based web user interface (UI) that will be deployed to the server-side as an basic proof of integration concept.
This project and the final project rely heavily on the components from Project 1. You are encouraged to make sure your implementation of Project 1 is sound and resonably tested (within the bounds of the end-to-end scenario) prior to doing too much work on Project 2. You should continue to maintain your JUnit tests for the earlier components while working on these higher level tiers.
We are continuing a theme that eLeague is complex and requires all levels of architecture and that eClub is simple and can leverage any and all shortcuts. The intent is to show how JavaEE can be used for projects of different sizes and demands. To inially meet satisfy this requirement, you must ...
Deploy eLeague as an EAR and eClub as a WAR
Implement separate DTOs for eLeague and re-use BOs as DTOs for eClub
Implement separate Impl and EJB modules for eLeague and combine Impl and EJB into a single module for eClub. The combined Impl and EJB module may also be combined with the WEB module for eClub WAR. It is easily possible for you to implement eClub with only a single WAR module at this time.
The project will continue along two parallel paths; eLeague and eClub. However, this time we will add several new Maven project types; EJB, EAR, WAR, Client (library) and (remote) Test. We will add all new project types to eLeague. We will add (or migrate to) the WAR type to eClub. These new modules will become siblings to your existing eLeague Impl -or- BO, DAO, and BLImpl leaf modules. For eClub, you have the option of:
making the WAR a sibling of the existing leaf Impl module
migrating the existing Impl project to be a WAR project (and remain with a single module for eClub)
making the WAR be the only module (i.e., no mid-level parent)
The new projects will depend on your legacy work. The remote interface of the EJBs will also require specific design of what gets externalized to the client. The remote clients do not share the same address space we had in Project 1 and cannot lazily access relationships. We also cannot afford to serialize the entire contents of a database full of related information. Data Transfer Objects (DTOs) will be part of the EJB remote interface design.
You have finished a significant amount of eLeague during Project 1; the O/R mapping and core business logic of a non-trivial business model. You will now host the data access tier and business logic within EJB component(s). These EJB component(s) will directly supply the EntityManager, control transaction boundaries, supply a local interface, and other features (like security access control) used in the follow-on project. These EJB component(s) may directly implement an RMI remote and JAX-RS web interfaces. However, we will focus the bulk of our remote interfaces on modern WEB-based, REST-like, JAX-RS remote interfaces. Most of the remote access will be provided from the WEB tier using JAX-RS and a browser-based Web UI. You are to deploy eLeague using an EAR and eClub using a WAR.
The business logic for eClub will require additional work as well. With a remote interface for eLeague in hand and the ability to either simulate or operate with a live instance, we can now complete the rest of the the business logic that will also be hosted within an EJB tier to keep from significantly impacting any tests from project 1. The testing of eClub can always assume that a local instance of eLeague can be deployed locally at any time.
You may develop your Web UI in an alternate environment. However, it must be deployed as part of the application that runs within JBoss/Wildfly.
As with the previous project, the use of the name eSport, eLeague, and eClub within the project specification are to be taken as placeholders. Please make an effort to uniquely name your directories, components, Java packages, JNDI names, etc. to help logically separate the parallel project implementations.
There should be no use of System.out.println() in the code and all implementations must use a logging API with the log4j logging provider. You may leave debug in your code, but this should be able to be turned on/off with the proper logging priority changes in the log4j.xml configuration.
It may be helpful to browse the grading criteria at the end of this assignment before reading through the specifications. This will give you a better idea of what has to be completed to achieve a passing grade. A perfect score for the assignment will require that the entire specification be implemented. A passing score will require the demonstration of understanding of specific concepts covered. You can conceivably do well in the grading if you tackle each of the technical areas at least once. Example: Don't focus so much on completing the steps of the end-to-end that you ignore the transaction and Web UI aspects of the assignment. Of course, the opposite is true. Don't ignore the end-to-end scenario and demonstrate functionality in a vacuum. The end-to-end scenario should provide plenty of chances to demonstrate required technical parts of this assignment.
Continue to provide all functionality from Project 1; especially the unit tests as you revise your earlier implementations For example, you should continue to have the ability to run the unit and end-to-end tests implemented in project 1. Make sure you design your project 2 testing such that it does not depend on database residue from project 1 tests.
Create a Maven module layout that will support the additional architecture layers of the assignment. It is intended that eLeague be implemented with a multi-module approach (to mimic a complex application) and eClub be implemented with very few (possibly one) modules (to mimic a simplistic application).
To implement the full project 2 assignment, you will eventually need your existing implementation module(s) and new modules to implement a remote client, EJB, WEB, EAR, and test concepts.
League Implementation Module(s)
These are your existing implementation module(s) from project 1. They are intended to remain mostly unchanged when working on project 2. They represent your core data and business logic and business decisions in getting this far. We want to keep project 2 focused on the technical aspects of deploying this capabilty to the server-side and leave the detailed business decisions behind.
LeagueClient Module
This new module is going to make available to external clients of eLeague. This will contain data transfer objects (DTOs) and JAX-RS client code to make implementing the client calls easier.
LeagueEJB Module
This new module will host the EJB components and define a META-INF/persistence.xml that is appropriate for use on the server-side. Most of the implementation of these components will be based on the project 1 implementation.
LeagueWAR Module
This new module will host the JAX-RS interface for eLeague and any Web UI that is developed.
LeagueEAR Module
This new module will package the EJB(s), WAR(s), and necessary JAR(s) in a single deployment to the server. It will define identity information for the EJB and WAR that will impact base JNDI and URI name paths generated to access the hosted components. It will also be a location where we exclude unecessary dependency JARs to trim the EAR of unecessary artifacts.
League Remote Test Module
This new module will deploy the EAR to the server and execute integration tests that use remote interfaces to verify eLeague functionality.
To implement the project 2 assignment, you will need a WAR module. You can create a WAR module that is a sibling to your existing implementation module or change your existing implementation module into a WAR. Since there are no clients of eClub (other than the eClub and end-to-end eSport tests that can be hosted within the WAR module) there will be no need to expose a client interface.
Club Implementation Module
This is your existing implementation module from project 1. It is anticipated that you will enhance your existing business logic to become EJBs and host the existing code in a module that supports EJB. That module can be the WAR.
ClubWAR Module
This new module will host the JAX-RS interface and Web UI for eClub and any supporting components and artifacts necessary to support them. It is anticipated that you will convert your existing module from project 1 into a WAR. However, the only core requirement is that you deploy eClub as a WAR -- whether you choose to add separate EJB modules or not.
Create an EJB tier for both applications to host your server-side, transactional, and persistence logic. Security will be added in the next project.
The EJB tiers must host your persistence unit and supply a set of EJB components that will provide access to your server-side logic.
The eLeague EJB tier must be hosted in a separate EJB module and deployed to the server in an EAR. A remote Test module must deploy the EAR as part of its integration testing (IT).
LeagueTestUtilEJB
This candidate support EJB will primarily host the LeagueTestUtilImpl business logic to support remote testing requirements. Its biggest role is to perform database initialization and ingest. It can be the target of early "hello world" test calls without poluting your primary application classes.
ClubMgmtEJB
This candidate EJB will primarily host the ClubMgmtImpl business logic where clubs can define their Teams and Venues in eLeague.
LeagueMgmtEJB
This candidate EJB will primarily host the LeagueMgmtImpl business logic where the seasons and divisions are managed.
ContestMgmtEJB
This candidate EJB will primarily host the ContestMgmtImpl business logic where day-to-day contests are managed.
The eClub EJB tier may be part of a separate module but must be deployed within a WAR. The WAR module must deploy the WAR as part of its integration testing (IT). Note that the candidate EJBs in this case are shown to be the Impls from project 1 turned into EJBs. This is to highlight that EJBs are just annotated POJOs (plain old Java objects) and this approach is a simplification of implementation over the eLeague approach.
ClubTestUtil
This candidate support EJB will primarily host the ClubTestUtilImpl business logic to support remote testing requirements. Its biggest role is to perform database initialization. It can be the target of early "hello world" test calls without poluting your primary application classes.
MemberMgmtEJB
This candidate EJB will primarily host the MemberMgmtImpl business logic where people assiciated with the Club can sign up and provide their identity information.
OrgMgmtEJB
This candidate EJB will primarily host the OrgMgmtImpl business logic where the seasons and divisions are managed.
Note the difference in how the two approaches addressed the EJB hosting the BLImpl business logic. In the first/more complex case -- the EJB and BLImpl were separate classes with the EJB delegating to the BLImpl class. This allowed us to highlight what the EJB is adding to the solution. In the later/simpler case, the BLImpl became an EJB. This allows us to highlight how the EJB can contain our business logic can be subject to normal POJO unit tests if desired.
The EJB tier must host your business logic and data access tiers. The EJBs must define injections and initialize implementation objects and provide a remote interface. Configuration and initialization can be via deployment descriptors, brute force/manual code in @PostConstructs, or indirectly using CDI. It is your choice. Transaction scope will be added as a part of a separate requirement.
The candidate EJBs are shown with possible dependencies between them. @Local interfaces are meant to identify interfaces that can easily support pass by reference and lazily loaded entities. @Remote interfaces are meant to identify interfaces where calls are likely coming from an external context (i.e., pass-by-id or pass-by-value serialization) and the responses must be complete. Each @Local interface could be the interface defined by the Impls.
Each of the EJB components will need to expose targeted behavior required to complete the end-to-end scenario.
The candidate ClubMgmtEJB may need to support calls from LeagueMgmt to form seasons for its teams.
Each of the EJB components will need to expose targeted behavior required to complete the end-to-end scenario.
The candidate OrgMgmtEJB may need to make remote calls to eLeague and eLeague has been depicted as providing a client module for such use.
Remote interface to the two applications will be demonstrated using two technologies: Java Remote Method Invocation (RMI) and HTTP/REST-like (using JAX-RS). Any data transferred using either will be part of the Data Transfer Objects (DTOs). DTOs used with RMI will need to be Java Serializable. DTOs used with JAX-RS will need to be marshaled as XML or JSON.
DTOs and and any classes written to support the remote interface will be placed in a JAR module that can be easily used by clients. Since eClub does not have any clients and we want to limit its complexity -- only eLeague will require a separate client module to host these classes.
We will limit our required use of RMI to eLeague and only to the eLeagueTestUtilEJB. This interface will be made accessible to remote clients of the application server using JNDI.
The commands to reset and populate the eLeague database must be accessible to a remote client using RMI.
The primary focus for implementing remote interfaces will be HTTP/REST-like interfaces using JAX-RS (server and client APIs). The EJBs already provide an injectable component to complete the desired functionality. It is intended that you add a set of one or more classes that expose access to this functionality using JAX-RS.
A candidate set of resource collection URIs is shown below. Add a {resourceId} to address a specific resource from the collection. Add an additional property name or query parameter to address a specific property of the resource. Use the appropriate verb (GET, POST, PUT, DELETE), query parameters, and payloads to make the intended requests. Make use of appropriate status codes (e.g, 200, 201, 400, 404, and 500) to communicate the results.
There is no requirement that you specifically make use of the specific URIs listed or break root level resources into separate JAX-RS classes. It is your choice. It is suggested that all URIs be exposed under the "/api" root URI for the targeted application context. The details here are provided as concrete suggestions. In the end, the primary requirement for remote interfaces is:
All server-side methods methods invoked remotely by the end-to-end scenario (excluding reset() and populate() for eLeague) must be accessed through HTTP/REST-like interfaces implemented with JAX-RS.
The diagram below shows an abstract JAX-RS layer that should be deployed as one or more classes in the WAR. Since eLeague is required to have separate DTO classes from BO classes -- you will need to create a mapping capability from DTOs to BOs for request payloads and BOs to DTOs for response payloads.
The following is a list of candidate URIs for eLeague that could be exposed below "/api".
clubs - clubs in league
clubs/{clubId}/venues -- venues for club
clubs/{clubId}/teams -- teams for club
contests - contests in league
divisions - divisions in league uniquely identifiable outside of season
divisions/{divisionId}/teams - team seasons within division
seasons - seasons in league
teams - teams within league uniquely identifiable outside of club
tests - open-ended base resource to support testing
The diagram below shows an abstract JAX-RS layer that should be deployed as one or more classes in the WAR. Since eClub is required to reuse BOs as DTOs, you will not need to perform different class mapping. However, to cleans the BO of persistence parasites and to reduce information expressed, you may need to create POJO copies of the BOs when going from the BO to DTO role.
The following is a list of candidate URIs for eClub that could be exposed below "/api".
members/parents - parents in org
members/players -- players in org
members/coaches -- coaches in org
teams -- teams in org
teams/{teamId}/players -- players on team
teams/{teamId}/coaches -- coaches for team
teams/{teamId}/managers - managers for team
tests - open-ended base resource to support testing
Create or identify classes that will express information passed between client and server within the remote interfaces.
DTO classes passed between client and server using RMI interfaces must be Java Serializable.
DTO class state passed between client and server using the JAX-RS interfaces must be either marshaled using XML or JSON.
XML marshaling should be implemented with JAXB. JSON marshalling can be implemented with JSONB, but it is recommended to use jackson2 since JSONB is not supported in Wildfly until we reach JavaEE 8 compliance. Client-side JSONB and server-side jackson will work 95% of the time -- but there are some differences.
<!-- JSON wiring for RESTEasy JAX-RS provider (javaee7)--> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jackson2-provider</artifactId> </dependency>
eLeague will be implemented using separate DTO and BO classes to maintain separation between client interface and implementation.
eLeague DTO classes must be a distinct set of classes specifically designed to pass data between client and server.
eLeague BO/entity classes may not be used as DTOs.
The following is a list of candidate DTOs including an ErrorDTO that can be used to express server-side error responses. Requests likely have most of the same BO data filled in. Responses likely are redacted down to only what is necessary to express an ID and possibly its name. e.g., when listing the location for a Contest the ID and name of the related Venue should be sufficient. If they want more information about the Venue -- that can be a request of the venues resource and not the related contest.
AddressDTO
ClubDTO
ContactDTO
ContestDTO
DivisionDTO
DivisionsDTO
ErrorDTO
TeamContactDTO
TeamDTO
TeamSeasonDTO
VenueDTO
For ease of client access (including IT tests), you will write a JAX-RS Client helper class that encapsulates the URI paths, methods, query parameters and payloads of all HTTP/REST-like requests and also encapsulates determining the status and extracting any result payload.
Most of our external client calls to eLeague will be accomplished through JAX-RS and will use DTO classes. For that reason, we will need to package the client classes in a separate module for use in eClub and in any IT tests within eLeague.
eLeague shall provide a class that uses the JAX-RS Client API to make HTTP/REST-like calls to eLeague.
eLeague shall provide a separate client module that contains the JAX-RS Client class and interface DTOs.
There is no explicit requirement for an eClub Remote client since this application has no external application clients within the scope of the project. However, you may find it helpful to still write a JAX-RS client class for use with IT tests like the end-to-end scenario.
A placeholder step was included in project 1 that we could not yet complete because eLeague did not have a remote interface at that time. Now that eLeague has a remote interface to use (for eClub development and testing as well), we can implement getting the team season for a member of eClub.
eClub and eLeague manage independent concepts of a team and their IDs are different. eClub must maintain not only its internal ID but also the ID used by eLeague so that users of eClub can easily locate information for their team.
Add transaction properties to the EJBs.
Transaction Scope - all session bean methods in these two applications should require a transaction unless otherwise justified.
Transaction Integrity - Create a demonstration of transactions integrity and the capability of rollback by implementing a scenario that adds something to the database and then a *follow-on* rollback causes the changes to be undone. The state of the database must remain consistent once complete.
To satisfy the transaction demonstration requirement, you must actually store something in the database and then later in the transaction roll it back based on some decision. If you make the decision before you store the information -- you are not satisfying the requirement. It is common for students to implement a wrapper around an existing method that takes a collection and populate the collection with good data up front and bad data at the end. The good data that got successfully processed should get rolled back once the bad data is processed. Create remote methods that can tell your remote test client that this actually occured.
Add separate server-side Web UI(s) to demonstrate integration between the Web UI and EJB Tiers. To meet this requirement -- you only have to implement *ONE* of the following in either eLeague or eClub.
The requirement for the Web UI is a single action (display a page, make a request, and display results). You can implement more actions, but all the steps in the end-to-end must still be implemented using remote interfaces and automatically tested with JUnit.
Test Admin
reset All tables
populate tables (using Ingestor)
League Officials
Club Coordinators
add Venue for Club
add a Team for Club
add a contact for a Team
assign a Team to a Division
Division Coordinators
report a Score
Anonymous User
show Division Standings
The following sketch of directory structure can be used as a starting point for your overall application. It assumes you already have either a consolidated Impl -or- a BO, DAO, and BLImpl set of projects in place from Project 1. You will *not* have the option of consolidating the EJB, WAR, EAR, Client and remote Test into a single project for eLeague. They must be implemented as separate projects with proper dependencies between them declared. You *do* have the option of merging all modules into a single WAR module for eClub or many of the alternatives as long as eClub is deployed as a WAR.
Figure 23.1. Candidate Source Module Structure
`-- eSport |-- eLeague | |-- eLeague (module(s) from project1) | |-- eLeagueEJB | | |-- pom.xml | | `-- src | | `-- main | | |-- java | | | `-- eleague | | | `-- ejb | | `-- resources | | `-- META-INF | | |-- beans.xml | | |-- persistence.xml | | |-- (ejb-jar.xml) | | `-- (jboss-ejb3.xml) | |-- eLeagueWAR | | |-- pom.xml | | `-- src | | `-- main | | |-- java | | | `-- eleague | | | |-- rs | | | `-- web | | |-- resources | | `-- webapp | | |-- WEB-INF | | | |-- beans.xml | | | |-- web.xml | | | |-- (jboss-web.xml) | | | `-- (content) | | `-- index.jsp | |-- eLeagueClient | | |-- pom.xml | | `-- src | | |-- main | | | `-- java | | | `-- eleague | | | |-- dto | | | `-- client | | `-- test | | `-- java | | `-- eleague | | `-- dto | |-- eLeagueEAR | | `-- pom.xml | |-- eLeagueTest | | |-- pom.xml | | `-- src | | `-- test | | |-- java | | | `-- eleague | | | |-- rmi | | | `-- rs | | `-- resources | | |-- jndi.properties | | `-- log4j.xml | `-- pom.xml |-- eClub (many options) | |-- (eClubEJB -- could be merged with or separate from WAR) | |-- eClubWAR | | |-- pom.xml | | `-- src | | +-- main | | | |-- java | | | | `-- eclub | | | | +-- bo | | | | +-- bl (or ejb) | | | | +-- client | | | | +-- rs | | | | `-- ui | | | |-- resources | | | | `-- META-INF | | | | |-- persistence.xml | | | | `-- (ejb-jar.xml) | | | `-- webapp | | | |-- WEB-INF | | | | |-- beans.xml | | | | |-- web.xml | | | | |-- (jboss-web.xml) | | | | |-- (jboss-ejb3.xml) | | | | `-- content | | | `-- index.jsp | | `-- test | | |-- java | | | `-- eclub | | | |-- ... | | | |-- bl | | | | `-- eClubEndToEndTest.java | | | `-- client (*IT.java) | | | `-- eSportEndToEndIT.java | | `-- resources | | `-- jndi.properties | `-- pom.xml `-- pom.xml
The end-to-end scenario will test your end-to-end application in a "happy path" scenario. Any white-box or black-box testing of alternate and error paths would be appropriate to put in the separate unit and IT test cases.
Provide a JUnit integration test that verifies the eLeagueEAR can be deployed to the server, including successful deployment of the persistence unit and contained EJBs.
Provide a JUnit unit test that verifies successul marshaling and demarshaling of an eLeague DTO class using Java Serialization. You may create a DTO class specifically for this test if none of your DTOs are used in your RMI interface.
Provide a JUnit integration test that verifies a successful JNDI lookup and RMI communication with an eLeagueEJB.
Provide a JUnit unit test that verifies successul marshaling and demarshaling of an eLeague DTO class to/from XML or JSON.
Provide a JUnit integration test that verifies a successful HTTP communication with an eLeagueEJB.
Create a single JUnit integration test case (e.g., eSportEndToEndIT) with a single @Test method that walks the two applications through the following end-to-end scenario. You may create a separate IT test in eLeague while developing the eLeague portions but the final end-to-end scenario should be placed in eClub since eClub depends on eLeague.
Deploy the applications
Reset all eLeague tables (using the LeagueTestUtilEJB)
Populate the eLeague tables (using LeagueTestUtilEJB and the LeagueIngestor)
Reset all eClub tables (using the ClubTestUtilEJB)
Populate the eClub tables (using ClubTestUtilEJB) *if* anything necessary
Create a Club in eLeague (using the LeagueMgmtEJB). Assign the club representative to have the login of "user2"
Add Venue for Club in eLeague (using ClubMgmtEJB)
Create Parent in eClub (using MemberMgmtEJB). Assign this user login of "user3".
Create Player in eClub (using MemberMgmtEJB)
Add a Coach role in eClub (using MemberMgmtEJB)
Create a Team in eClub (using OrgMgmtEJB)
Assign Players to a Team in eClub (using OrgMgmtEJB)
Assign Coach to a Team in eClub (using OrgMgmtEJB)
Assign Manager to a Team in eClub (using OrgMgmtEJB)
Show team roster in eClub (using OrgMgmtEJB)
Add a Team for Club in eLeague (using ClubMgmtEJB)
Add a contact for a Team eLeague (using ClubMgmtEJB)
Assign a Team to an existing Division eLeague (using ClubMgmtEJB; division=U13-A, refid="Division-845")
Schedule a Season eLeague (using ContestMgmtEJB)
Project 3 placeholder
Project 3 placeholder
Show a Team Schedule in eClub (using OrgMgmtEJB)
Project 3 placeholder
Project 3 placeholder
Report a Score in eLeague (using ContestMgmtEJB)
Show Division Standings in eLeague (using LeagueMgmtEJB)
To be clear. The above scenario should be both automated with a single JUnit test. Any Web UI actions you implement can augment these actions but not replace them.
Your project will be graded primarily on the demonstration ability to implement concepts covered in this portion of the course. A perfect score will need to implement the full end-to-end scenario. A passing score will need to make sure to cover the grading criteria outlined below within the partial end-to-end implemented.
README provided that describes where each requirement satisfied and describes the non-obvious: 10pts total
Projects cleanly builds with Maven and deploys required modules: 12pts total
Project cleanly builds the required modules (JAR, EJB, WAR, and EAR) with Maven: 3pts
Build automatically deploys the applications to the server for IT tests: 3pts
Project successfully deploys a persistence unit within an EJB and WAR modules: 3pts
Project successfully deploys EJBs within an EAR and WAR modules: 3pts
Project 1 functionality: 10pts total
JPA entity mappings correct: 8pts
Project 1 end-to-end unit tests still exist: 2pts
EJB Tier: 15pts total
Demonstrated ability to configure EJB with necessary resources and/or dependencies: 5pts
Demonstrated ability to complete a call from the remote client to the database and back: 5pts
Correct use of stateless constructs: 5pts
RMI Interface: 9pts total
Demonstrated ability to define a @Remote interface and expose using JNDI: 3pts
Demonstrated ability to lookup a @Remote interface using JNDI: 3pts
Demonstrated ability to invoke an EJB method using RMI: 3pts
JAX-RS Interface: 17pts total
Demonstrated ability to marshal and demarshal payloads from/to XML or JSON into/from DTO classes: 5pts
Demonstrated ability to define an HTTP resource endpoint: 5pts
Demonstrated ability to define a resource URI path.
Demonstrated ability to use HTTP methods properly with respect to actions performed.
Demonstrated ability to map an input method parameter from URI and query parameters.
Demonstrated ability to use status codes properly with respect to result status.
Demonstrated ability to implement a JAX-RS client interface and invoke an HTTP resource endpoint: 5pts
Demonstrated ability inject an EJB into a JAX-RS class and delegate implementation to the EJB: 2pts
Web UI integration: 10pts total
Implementation of a browser-based, server-side Web UI: 3pts
Demonstrated ability to inject an EJB into the Servlet class to implement functionality behind the Web UI: 2pts
Implementation of at least one entire use case from prompt, request, action, and response: 5pts
Transactions: 7pts total
Explicit transaction attribute defined: 2pts
Proper demonstration of data flushed to DB being rolled back: 5pts
End-to-end Integration Test: 10pts total
Clean, easy to follow, sequence of steps through the end-to-end flow demonstrated: 10pts
The following table contains examples of where projects have lost points in the past. Of course, each project submitted can introduce new issues or different severity levels of the same issues. Do not treat this as a complete list.
Table 25.1. Sample Lost Points
README | |||
---|---|---|---|
Not provided | 10 | ||
README did not indicate where X was located and it was not obvious even after ... | 2 | ||
The WebUI is hard to navigate (fine) but README offered no assistance | 5 |
Projects cleanly builds with Maven | |||
---|---|---|---|
One of your IT tests assume the DB is setup correctly prior to running and that would only be true of we were running a common server database instance across our unit and IT tests. | 2 | ||
Testing does not produce consistent results - out of four runs of mvn install the process failed twice and succeeded twice. | 2 | ||
Build was not portable. I had to make changes. Sending me a copy of this beforehand would have caught this. | 2 |
Project 1 functionality | |||
---|---|---|---|
DB schema is under-constrained | 1 | ||
Project 1 end-to-end scenarios no longer exist/run | 2 |
EJB Tier, remote interface, and EAR deployment | |||
---|---|---|---|
Attempting to set the state of a @Stateless EJB. What do you think will happen to that state when you get a different bean instance the next time you call? | 5 | ||
With auto-create DDL turned on (hibernate.hbm2ddl.auto" value="create") your application deletes all data when redeployed. Deleting all data should be restricted to only an explicit call to resetAll(). | 5 | ||
Not separating a single call into separate transactions to drop and then create schema. First action may fail when not exist. | 2 | ||
You have methods that will only work in a @Local interface as part of your @Remote interface | 2 | ||
Reusing business logic classes instead if EJB components - repeating the same work of the reusable EJB component in each EJB that needs to reuse the functionality. Inject @Local interfaces. Do not repeat instantiation and setup of identical business logic/DAO classes. | 2 | ||
Injest was not implemented within a deployed EJB. It was mistakely implemented the same as project 1 in a JUnit client. | 5 | ||
You have not integrated the two applications at the EJB remote interface level. The second application does not make a single remote call to the first application. All interaction is occcuring outside of the server-side and results passed in. | 5 |
WAR/EJB deployment | |||
---|---|---|---|
You are deploying EJBs from the first application within your second application's WAR. Look at your built/deployed artifact and correct dependencies. | 5 |
Web UI integration | |||
---|---|---|---|
UI does not provide a path to satisfy a step in the end-to-end scenario (that the IT test shows works). | 2 | ||
EJB/Impl functionality called by UI fails and is not exercised by end-to-end IT test | 2 | ||
This is a real bust when tested on the deployment platform. This should have been easily noticed. Pressing resetAll() results in the following error displayed. | 5 |
Transactions | |||
---|---|---|---|
Transaction scope not explicitly defined for EJB. You are accepting container defaults. | 1 | ||
No attempt to demonstrate transaction rollback | 10 | ||
Scenario shows business logic check but not a rollback of actions (store) to a transactional resource (DB). The requirement called for you to persist something all the way to the database - such that if you stopped in a breakpoint you would see the data - and then have the data thrown away due to a rollback. | 7 |
End-to-end Integration Test | |||
---|---|---|---|
Poluted, hard to follow, too much extra stuff | 2 | ||
Missing resetAll and populate at start of scenarios | 5 | ||
Missing step X | 1 | ||
No. I want different functionality in this step | 1 |
Table of Contents
Secure access to EJB and Web applications
Decouple business logic from common tasks
Provide for asynchronous processing.
Re-use the eLeague and eClub implementations from projects 1 and 2.
Implement authentication and access restrictions for EJB methods and WEB URIs thru declarative and programmatic APIs.
Implement authentication for RMI and HTTP(s) clients.
Implement DTO validation using interceptors.
Implement a filterable, publish/subscribe capability with portable payloads between applications.
Schedule and invoke certain behavior based on an EJB timer.
The project will build on the core implementation from Projects 1 and 2. We will mostly extend existing projects with security and asynchronous logic.
JavaEE defines authentication and authorization to be independent of the overall API and capability. JBoss and other application servers provide default mechanisms behind the scenes to implement these features -- that can make it simple and easy to demonstrate. A switch to more realistic and sophisticated mechanisms should require no change to JavaEE-compliant application code. We will use the simple, default "other" security-domain defined within the standard JBoss installation. This uses the RealmUsersRoles login-module -- which is powered by two property files supplied and pre-populated by the course server files from ejava-wildfly(version) in your course examples source tree. You unzipped this file into your application server configuration as part of course setup and it would be wise to repeat that to make sure your server configuration is up to date with any changes added since then.
Table 26.1. User Credential Files
File | Description |
---|---|
${jboss.server.config.dir}/application-users.properties | defines username=password lines that list the login username and hashed password |
${jboss.server.config.dir}/application-roles.properties | defines username=role1,role2 lines that list the login username and assigned roles |
${jboss.server.config.dir} is a reference to an internal JBoss variable that references the "standalone/configuration" directory.
$ ls standalone/configuration/application*.properties standalone/configuration/application-roles.properties standalone/configuration/application-users.properties
We are going to have several types of user roles. Some of the users will have zero, one, or more of these roles. Because of the static nature of our demonstration authentication solution, all users will have a login configured before the application is even deployed to the server.
Table 26.2. Application Roles
Role | Description |
---|---|
(anonymous) | These users can view standings and schedules in league |
eleague-sys | Authorized to publish to eLeague topic |
eleague-user | Authorized to subscribe to eLeague topic |
eleague-admin | Authorized to reset and populate the database |
eleague-coord | Authorized to create season, divisions, clubs, and schedule contests. |
eleague-clubcoord | Authorized to create teams, provide contacts for the teams, and assign them to divisions. |
eleague-divcoord | Authorized to report scores. |
eclub-admin | Authorized to run ClubTestUtil operations. |
eclub-member | Authorized to obtain team rosters and update contact and role information. |
eclub-coord | Authorized to create teams and assign players, coaches, and managers. |
If a user has a login for one application, they will use the same account to access the other application (e.g., user2 has both eleague-clubcoord and eclub-coord roles).
The "sys" accounts are meant to be used by the application code when actions taken require an authenticated user but there is no caller context (e.g., async callbacks) or the identity of the incoming caller is not appropriate for the outgoing call being triggered (e.g., elevated permission access).
Table 26.3. User Roles
User Login | Roles |
---|---|
known | (no roles) |
eleaguesys1 | eleague-sys |
eclubsys1 | eleague-user |
admin1 | eleague-admin,eclub-admin |
lmtucker | eleague-coord |
user2 | eleague-clubcoord,eclub-coord,eclub-member |
user3 | eclub-member |
jtflynn | eleague-divcoord |
Based on the test data, the following are some other logins that may be useful. They have been added to your users and roles property files and are referencs either in the ingested XML file or part of the end-to-end demonstration steps.
Table 26.4. Other Users/Roles
Role | Users |
---|---|
eleague-clubcoord | tbswanson, kdromero, sthart, lmfuller, rgrobbins, gchamilton, avschultz, jcbell, user4 |
eleague-divcoord | jddavidson, whlee, pjjimenez, ksharrison, acrussell, ckriley5687 |
To clarify, your application will have a static set of logins and will dynamically ingest a set of Contacts at scenario startup. More Contacts and Members will be added during the scenario. A user with a login and no Contact/Member info within the database can login, but won't be able to do anything meaningful to them personally. A user with a Contact/Member defined and no login won't be able to access the protected areas of the system. Normally the login would be created at the same time the Contact/Member is added. Except for your JBoss configuration and your add user logic, no other part of your project should be aware of this tradeoff made for class project simplicity.
Some actions are open to any users; authenticated or not. Authentication will be performed using a JNDI login for RMI, BASIC for HTTP web services, and FORM for Web UIs. All users will have a password of "password1!".
We will optionally (for the assignment) use HTTPS to provide confidentiality between our client and server for communications. The server has a keystore for identity. The JUnit tests can conveniently use that as a trustStore through the base path defined by the "jboss.home" property.
# build/dependencies/pom.xml
<java.truststore>${jboss.home}/standalone/configuration/application.keystore</java.truststore>
...
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<javax.net.ssl.trustStore>${java.truststore}</javax.net.ssl.trustStore>
Table 26.5. HTTPS Keystore(s)
File | Description |
---|---|
${jboss.server.config.dir}/application.keystore | Self-signed JKS keystore with server certificate. Also also used as client trustStore during testing. |
jboss.home is a property you should define in your "$HOME/.m2/settings.xml" file and have it reference your installation directory for JBoss/Wildfly.
# .m2/settings.xml
<profiles>
<profile>
<id>wildfly13</id>
<properties>
<jboss.home>.../apps/wildfly-13.0.0.Final</jboss.home>
</properties>
</profile>
...
<activeProfiles>
<activeProfile>wildfly13</activeProfile>
<activeProfile>h2db</activeProfile>
</activeProfiles>
Although essential in securing communications, SSL and/or HTTPS is not a requirement for implmenting this assignment. The instructions above are provided for students wishing to satisfy a key but not very noticeable portion of a secure interface.
For asynchronous activity, we will implement a Schedule JMS Topic where eLeague will publish messages related to team schedules. eClub will subscribe to the topic using a Message Driven Bean (MDB) to keep members up to date on schedule changes. The JMS topic has already been added to your JMS Server within JBoss and has been assigned both an internal and external JNDI name. You will have to design the message type, message properties, and payload of the messages sent on that topic in order that the message carries a partable payload and can be filtered for subscriber-specific criteria via a JMS selector. Your DTOs already express a portable expression of data. Each club will only want to process messages for their teams and only events that impact upcoming schedules (i.e., not scores of completed Contests).
<jms-topic name="esport-eleague-contests"
entries="java:/topic/ejava/projects/esport/eleague-contests
java:jboss/exported/topic/ejava/projects/esport/eleague-contests"/>
eLeague will use EJB Timers to send out schedule reminders for upcoming Contests.
Provide all functionality from Projects 1 and 2.
Enhance eLeague with access restrictions.
Assign the EJBs and WARs to the "other" security domain
Restrict access to WEB URIs in WARS
All API calls must allow authentication.
Use BASIC login-config for API calls.
Use of CONFIDENTIAL (HTTPS) is not required for this project but can be easily added if you have time.
Restrict access to the EJB methods to appropriate read/write roles using declarative security.
Administrators may run the test utility functions like reset() and populate().
League Coordinators may create clubs, seasons, and divisions as well as schedule contests. They may also perform duties normally performed by club and division coordinators -- like report scores.
Club Coordinators may create venues, teams, and assign teams to divisions
Division coordinators may report scores
Perform additional programmatic security checks to verify the right user is accessing the appropriate information.
Club coordinators may only manage teams and venues associated with their club.
Division coordinators may only report scores for contests in their assigned division. Note that the League Coordinator should be able to report a score for any division.
League coordinators may report scores for any division without being the coordinator for that division.
Allow any user to perform read operations.
Use Run-As to assign application code a role sufficient to complete its required actions when there is no calling client identity.
Enhance eClub with access restrictions.
Assign the EJB and WARS to the "other" security domain.
Restrict access to the EJB read/write methods to appropriate roles using declarative security.
Administrators may run the test utility functions like reset().
Organization coordinators may create teams and members/roles. They can also assign players, coaches, and managers to teams.
Members may update contact information and obtain roster information.
Perform additional programmatic security checks to verify the right user is accessing the appropriate information.
Members are accessing rosters for teams they are associated with.
Use Run-As to assign application code a role sufficient to complete its required actions when there is no calling client identity.
Extend your existing client code to optionally accept credentials. No credentials provided should invoke a remote method/URI as an anonymous user. Credentials provided should invoke a remote method/URI using the identity associated with the credentials.
For RMI -- dynamically add security principal and credential properties to your runtime JNDI lookup.
For JAX-RS -- dynamically add a "Basic" Authentication header (ideally using a JAX-RS ClientRequestFilter) to each HTTP(S) request.
Extend your existing remote Test project to address new authentication requirements.
Add a valid login to your existing tests to re-enable them under the newly secure environment.
Add a new unit test case that verifies the access controls of the protected and open methods. This means that -- after fixing access issues in the previous bullet -- you purposely attempt to violate access restrictions from a new JUnit test to show a test of access control.
Please see the grading criteria to identify specific scenarios your JUnit tests should address in this area.
Enhance Web-UIs with authentication and access restrictions for any use case you implement requiring access control. Note that your selected use case may not require an authenticated user -- which would render this section not relevant.
Assign the WAR to the "other" security-domain.
Restrict access beyond the main page to users with the appropriate roles. You may use FORM or BASIC authentication.
Permit only users to only ask the EJB tier for information that is associated with their login. For example, a Club Coordinator should not have to tell the Web UI what club they are managing. It should be derived from their identity and information in the database.
Co-hosting the Web-UI and API in the same WAR will restrict you to using the same authentication mechanism for both. Since we are suggesting use of BASIC for API calls -- we would use BASIC for the Web-UI as well. That functionally works except the use of BASIC and the way the HTTP protocol and browsers work -- it makes it hard to logout to change identity. Keeping the project within scope -- this may limit the number of steps you implement with Web-UI for the end-to-end scenario.
Implement a "thin slice" of capability to demonstrate JavaEE interceptors using validation.
Define javax.validation constraints on at least one of your DTOs.
Implement a JavaEE interceptor that gets a Validator injected, intercepts all calls of your EJBs, and runs the validator against each parameter input to the EJB method.
The interceptor can have no knowledge of the specific EJB, method, or parameters it is intercepting and validating.
The EJB can have no knowledge of the interceptor and should do no work to validate the DTO itself.
In this portion of the project we are going to send and receive a JMS message using publish/subscribe JMS techniques.
Extend your eLeague EJB implementation to publish JMS messages whenever a Contest is scheduled, changes (e.g., scores posted), or for whatever reason the application has for notifying subscribers. You may want to place an artificial limit on the number of messages sent to help keep from being unnecessarily overwhelmed in the development environment. You can decide on any thresholds used.
Use the esport-eleague-contests topic (JNDI names: java:/topic/ejava/projects/esport/eleague-contests and java:jboss/exported/topic/ejava/projects/esport/eleague-contests)
Design your JMS Message to carry a portable payload. You can use any JMS Type you wish. However, know that subscribers will not likely have your implementation JARs with Entity classes. Use of DTO classes works well here.
Design your JMS Message to be filterable using a JMS selector. You can use any properties you wish. However, know that some subscribers will be filtering on such things as the impacted club, team, or type of event.
Have your eLeague EJBs publish JMS Messages to the topic when a Contest changes state (created, canceled, completed).
This means that there are several reasons for a Contest to be sent and that reason should be a filterable part of the overall message properties using a JMS selector.
Implement a Message Driven Bean within eClub to subscribe to contests that are new, rescheduled, or reminders; but not completed (i.e. it should have a selector that causes the MDB to not be told about scores being reported) that invole the specific club. [Note:] The ability to reschedule a contest was not part of the functional requirements and need not be implemented at this time. Just account for the future event and the relevance to the club in your selector.
Use a JMS Selector for the MDB to limit the types of messages consumed.
Obtain the e-mail address for each individual associated with the team effected by the event. Print a textual message and the address list to the server log.
You do not have to physically send an e-mail as a part of this assignment. The key point of this requirement is for your MDB to be able to succesfully access a protected EJB method without a calling client context present.
Implement an EJB Timer that will allow eLeague to automatically wake-up and send out schedule reminders for contests that have not yet finished. Depending on the provided test data, you might want to limit the number of reminders sent. The main goal of this requirement is that you make use of an EJBTimer and that you trigger the JMS filtered MDB logic in eClub.
Your timer can be declarative or programmatic. However, the programmatic approach is recommended so that -- with a remote interface -- your IT integration test can have control over when the events should start and end.
The end-to-end scenario will test your end-to-end application in a "happy path" scenario. Any white-box or black-box testing of alternate and error paths would be appropriate to put in the separate unit and IT test cases.
Provide JUnit IT tests that verify the EJB functionality of eLeague accessed through its remote interface using new access control restrictions. The test should demonstrate that authentication and access control is in place for this EAR-based deployed application, the authenticated identity of the caller can be accessed by the server-side code, and status of the call is properly conveyed to the caller. For both RMI and JAX-RS, there should be:
An example successful call of an access controlled resource using an authorized user properly authenticated and verified to be the correct value.
An example unsuccessful call (using the same call as above) of an access controlled resource using an unauthorized user
Proper indication to the caller that the caller was or was not authorized to make the call (e.g., exception thrown or status returned)
Provide JUnit IT tests that verify the EJB functionality of eClub using its new access control restrictions and ability to authenticate (if necessary) with eLeague. The test should demonstrate that authentication and access control is in place for this WAR-based deployed application. This is similar to above -- except must be demonstrated with eClub.
Provide a JUnit IT tests that demonstrates the functionality of the JavaEE interceptor/validator. This should include:
An example successful call using a valid DTO instance successfully validated by the interceptor/validator code.
An example unsuccessful call (using the same call as above) using an invalid DTO instance rejected by the interceptor/validator code. Validation must be performed with the Validation API, must be done within an Interceptor, and cannot be performed within the core Java logic within the called EJB.
An example of the caller receiving an indication of whether the call was rejected for security or validation reasons.
Implement the scripted use case below as an automated JUnit test and a selected portion manually accessed through the Web UI. The JUnit test/module must be delivered in a state that can be executed in a debugger -- whether directly within Eclipse (ideally) or using a remote debugging session to a Maven command-line build.
Since this end-to-end test spans both applications the most likely place to host is within eClub.
The applications deploy and/or started
eLeagueEJB sets a timer for schedule reminders
eClubEJB subscribes to pending contest events.
An eleague-admin (admin1) resets all eLeague tables (using the LeagueTestUtilEJB)
An eleague-admin (admin1) populates the eLeague tables (using the LeagueIngestor)
An eclub-admin (admin1) resets the eClub tables (using the ClubTestUtilEJB)
An eclub-admin (admin1) populates the eClub tables (using the ClubTestUtilEJB) if anything necessary.
The league coordinator(lmtucker) creates a Club in eLeague (using the LeagueMgmtEJB). League coordinator assigns the club representative (whoever you call them) to have the login of "user2" to match a login pre-defined in your Wildfly Server. This login has multiple roles assigned spanning both applications.
The eleague-clubcoord (user2) adds Venue for Club in eLeague (using ClubMgmtEJB). The "user2" login has been pre-defined to have the role "eclub-coord".
An eclub-member (user3) creates Parent in eClub (using MemberMgmtEJB). "user3" is a login pre-defined in your Wildfly Server with the role "eclub-member".
An eclub-member (user3) creates Player in eClub (using MemberMgmtEJB) for their child
An eclub-member (user3) adds a Coach role in eClub (using MemberMgmtEJB) to their identity
The eclub-coord (user2) creates a Team in eClub (using OrgMgmtEJB)
The eclub-coord(user2) assigns Players to a Team in eClub (using OrgMgmtEJB)
The eclub-coord(user2) assigns Coach to a Team in eClub (using OrgMgmtEJB)
The eclub-coord(user2) assigns Manager to a Team in eClub (using OrgMgmtEJB)
An eclub-member(user3) views their team roster in eClub (using OrgMgmtEJB)
An eleague-clubcoord (user2) adds a Team for Club in eLeague (using ClubMgmtEJB)
An eleague-clubcoord (user2) adds a contact for a Team eLeague (using ClubMgmtEJB)
An eleague-clubcoord (user2) assigns a Team to an existing Division eLeague (using ClubMgmtEJB; division=U13-A, refid="Division-845")
The eleague-coord (lmtucker) schedules a Season eLeague (using ContestMgmtEJB)
eLeague (eleague-sys) (thru ContestMgmtEJB) publishes schedule events to Schedule Topic
eClub (eclub-sys) (using LeagueListenerMDB) receives messages related to unfinished contests. It obtains the e-mail addresses for members associated with the team referenced in the message. It logs the message and e-mail addresses.
anonymous users views Team Schedule in eClub (using OrgMgmtEJB). eClub (eclub-sys) contacts eLeague (using LeagueMgmtEJB) for team season associated with eClub's team.
EJB Timer fires a callback in eLeague (using ContestMgmtEJB (eleague-sys))
eLeague (eleague-sys) (using ContestMgmtEJB) publishes reminder messages to the Schedule Topic (and received in eClub using LeagueListenerMDB).
The eleague-coord (jtflynn) (for division=U13-A, refid="Division-845") reports a Score in eLeague involving our club team (using ContestMgmtEJB)
anonymous user views Division Standings in eLeague (using LeagueMgmtEJB)
Your project will be graded primarily on the demonstration ability to implement concepts covered in this portion of the course. A perfect score will need to implement the full end-to-end scenario. A passing score will need to make sure to cover the grading criteria outlined below within the partial end-to-end implemented.
README provided that describes where each requirement satisfied and describes the non-obvious: 10pts total
Projects cleanly builds with Maven and deploys required modules: 10pts total
Project cleanly builds the required modules (JAR, EJB, WAR, and EAR) with Maven: 5pts
Tests are based on provided Wildfly configuration files (no rogue users): 3pts
Server-side tests do not rely on DB residue from unit tests: 2pts
Project 1 and 2 functionality: 5pts total
Client Security Login: 10pts total
Demonstrated ability to authenticate a client call using JNDI and RMI: 3pts
Demonstrated ability to detect that a client RMI call was unauthorized: 2pts
Demonstrated ability to authenticate a client call using HTTP and JAX-RS: 3pts
Demonstrated ability to detect that a client JAX-RS call was unauthorized: 2pts
EJB Security: 10pts total
Demonstrated use of declarative EJB access restrictions: 2pts
Demonstrated use of programmatic EJB access restrictions: 2pts
Demonstrated acceptance of an authenticated and authorized caller: 2pts
Demonstrated ability to determine the authenticated identity of an EJB caller: 2pts
Demonstrated denial of an unauthorized caller: 2pts
WAR Security: 10pts total
Demonstrated ability to have an HTTP URI client caller identity authenticated: 5pts
Demonstrated proper denial of an unauthorized HTTP URI client caller: 5pts
JavaEE Interceptors/Validator: 10pts total
Demonstrated ability to configure a JavaEE Interceptor around an EJB call: 4pts
Demonstrated ability to isolate the business and DAO logic from DTO validation: 3pts
Demonstrated denial of a call containing an DTO failing javax.validation: 3pts
JMS Message: 5pts total
Demonstrated ability to design JMS message to carry a portable payload: 3pts
Demonstrated ability to design a JMS message to be filtered by a selector: 2pts
EJB JMS Publisher: 5pts total
Demonstrated ability to inject JMS resources: 2pts
Demonstrated ability to construct a JMS message: 1pts
Demonstrated ability to publish a JMS message: 2pts
EJB MDB Subscriber: 5pts total
Demonstrated ability to receive a JMS message using an MDB: 2pts
Demonstrated ability to configure an MDB to filter JMS messages based on a selector: 2pts
Demonstrated ability to extract a portable payload from a JMS message: 1pts
EJB Timers: 10pts total
Demonstrated ability to declare an EJB callback method for a timer: 2pts
Demonstrated ability to define a timer to callback an EJB method: 5pts
Demonstrated ability to complete an EJB Timer callback: 3pts
End-to-End Integration Test: 10pts total
Clean, easy to follow, sequence of steps through the end-to-end flow demonstrated: 10pts
The following table contains examples of where projects have lost points in the past. Of course, each project submitted can introduce new issues or different severity levels of the same issues. Do not treat this as a complete list.
Table 29.1. Sample Lost Points
README | |||
---|---|---|---|
The WebUI is hard to navigate (fine) but README offered no assistance | 5 |
Projects cleanly builds with Maven | |||
---|---|---|---|
Using rogue users that are not part of the standard class setup in your end-to-end. | 3 | ||
Initial build fails. Looks to depend on DB schema bleedover between unit and IT tests. | 2 |
Project 1 and 2 functionality | |||
---|---|---|---|
Second application being deployed as EAR and not WAR | 2 | ||
Relying on persistence unit to create schema -- thus blowing away all DB data on deployment | 2 | ||
Missing scenario feature (e.g., wrong data) from project 2 end-to-end scenario. | 1 | ||
Attempting to set the state of a @Stateless EJB. What do you think will happen to that state when you get a different bean instance the next time you call? | 2 | ||
Not self managing schema. With the end-to-end having resetAll() in place, why did you rely on the JPA provider to initialize your schema? | 1 |
Client Security Login | |||
---|---|---|---|
EJB Security | |||
---|---|---|---|
Using credential logins for the JMS Connection from EJBs -- versus leveraging the @RunAs role | 1 | ||
Not relying on declaritive security to perform the role checks. You are also having the caller authorized for the role supply instance-specific information. For example, any division coordinator is allowed to report the score for any division. | 2 | ||
Not constraining authorized caller to manage only their information. Caller is passing references to information using identifiers that could be associated with any user versus "manage my stuff". By relying on those identifiers you are allowing them to "manage that stuff which may or may not be my stuff". | 3 | ||
EJB module not being associated with a specific security-domain. Relying on defaults. | 2 | ||
@RunAs takes a role -- not a principal | 1 |
WAR Security | |||
---|---|---|---|
Requiring login to pages that should allow anonymous access. | 1 | ||
WAR is not properly locked down. | 2 | ||
Mixed use of BASIC and FORM. When signing in to perform action a Basic authentication popup appears and logout no longer functions, need to close browser to log out. |
EJB JMS Publisher | |||
---|---|---|---|
Copied provided example wholesale and did not adjust to be your solution (e.g., features specific to the example are not required for project, comments specific to example are not appropriate for a project solution). | 2 | ||
Not closing resources (JMS 1.1) This eventually exhausts resources over time. | 2 | ||
Could not find testing of this anywhere to makeup for the fact that the end-to-end was not implemented | 2 |
EJB MDB Subscriber | |||
---|---|---|---|
Not implemented | 10 | ||
Using System.out versus logging framework or better error reporting | 1 |
Java SE JMS Listener | |||
---|---|---|---|
Did not provide your subscriber any credentials to interact with the server. | 2 | ||
Didn't work out of box. JMS topic mis-named | 1 |
EJB Timers | |||
---|---|---|---|
It would be a better design to treat the timer() callback as an interface facade and not the triggering implementation mechanism. You have combined EJB Timer, JMS publishing, and business logic within a single method. | 0 |
End-to-end Integration Test | |||
---|---|---|---|
Your configuration made it hard to run the end-to-end scenario in a debugger. All JNDI names and properties were solely expressed in the pom.xml rather than having suitable defaults in the IT test and overrides from the pom.xml. With that type of setup you did not have your pom.xml and surefire setup to allow remote debugging. | 2 | ||
Poluted, hard to follow, too much extra stuff | 2 | ||
Missing resetAll and populate at start of scenarios | 5 | ||
Missing step X | 1 | ||
No. I want different functionality in this step | 1 |
Copyright © 2019 jim stafford (jim.stafford@jhu.edu)
Built on: 2019-08-22 07:09 EST
Abstract
This presentation introduces the concept of the business logic tier, using it to form a user facade around the capabilities of the service, and to leverage business logic interfaces and tests as a form of executable requirements.
Introduce the architectural layer responsible for external simplicity and internal coordination
At the completion of this topic, the student shall
have more understanding of:
Role of business logic
How business logic can provide top-down requirements
How business logic tests can provide executable requirement verification
be able to:
Define business logic interface
Define business logic class
Distinguish business logic from data access concerns
Write a black-box test to act as an acceptance test
Potential tight coupling between clients and complex business objects
Too many method invocations between clients and business objects
Business methods exposed for misuse by clients
Hide complex interactions behind a simple client interface
Reduce the number of business objects exposed to the client across the network
Hide implementation, interactions, and dependencies of business components
Use a business logic class to encapsulate the interactions required with the business objects
Simplifies complex systems
May appear to be a no value pass-thru in simple systems
Should involve more than one business object per facade
Should have more than one facade per system
Decouples the business objects from being aware of one another
Improves perceived network performance for remote interfaces
Centralizes security and transactions in some cases
/**
* Purchasing handles payment of purchased products.
*/
public interface Purchasing {
/**
* Creates an account for the user to use in purchasing products.
* @param email
* @param firstName
* @param lastName
* @return the Account created with primary key assigned
*/
Account createAccount(String email, String firstName, String lastName);
/**
* Completes the purchase of the items in the user's shopping cart,
* empties the cart, and returns the total cost paid.<p/>
*
* Note that this capability is not yet fully defined.
* @param email
* @param password
* @return amount charged as part of this checkout
*/
double checkout(String email, String password);
}
/**
* The catalog maintains a view of the inventory known to our application.
*/
public interface Catalog {
/**
* Returns a list of products in the catalog chunked into page sizes.
* @param offset
* @param limit
* @return list of products matching the paging criteria
*/
List<Product> getProducts(int offset, int limit);
/**
* Adds the selected product to the users' shopping cart and returns
* the count of items.
* @param id
* @param validEmail
* @return number of items in cart
*/
int addToCart(int id, String validEmail);
}
@Entity
@NamedQueries({
@NamedQuery(name="blPurchasing.findAccountByEmail",
query="select a from Account a where a.email=:email")
})
public class Account {
public static final String FIND_BY_EMAIL="blPurchasing.findAccountByEmail";
@Id @GeneratedValue
private int id;
@Column(nullable=false, unique=true)
private String email;
@Column(nullable=false)
private String password;
@Column(nullable=false)
private String firstName;
@Column(nullable=false)
private String lastName;
...
@Entity
public class Product {
@Id @GeneratedValue
private int id;
@Column(nullable=false)
private String name;
@Column(nullable=false)
private double price;
@Column(nullable=false)
private int count;
...
@Entity
public class Cart {
@Id
private String email;
@OneToOne
@PrimaryKeyJoinColumn(referencedColumnName="email")
private Account account;
@ManyToMany
private List<Product> products = new ArrayList<Product>();
...
Start with simple empty class and then added details over time. Just the fact that we have the concept of a Product is valuable. The fact that it can be described using a single name is an extra detail. The fact that name description should be required is additional detail. These details come over time and should not be a barrier for proposing the data concept and moving forward with the early design.
Test-first approach
/**
* A user shall be able to establish an account with just a first
* and last name and a unique email address.
*/
@Test
public void establishAccount() {
log.info("*** establishAccount ***");
//the user will supply their email address, first and last name
String email="jharb@ravens.com";
String firstName="john";
String lastName="harbaugh";
Account account = purchasing.createAccount(email, firstName, lastName);
//they will get back a generated password to use as a login for the account
assertNotNull("no account returned", account);
assertNotNull("no password assigned", account.getPassword());
}
/**
* A user shall be able browse products in the catalog.
*/
@Test
public void browseCatalog() {
log.info("*** browseCatalog ***");
//the user will ask for product summaries in pages
int pageSize=10;
int offset=0;
List<Product> products = catalog.getProducts(offset, pageSize);
//they will receive <= a page size of product information
assertNotNull("no products returned", products);
assertTrue("no products provided", products.size() > 0);
//they can page thru the entire set
for (int i=0; products.size() != 0; i++) {
offset += products.size();
products = catalog.getProducts(offset, pageSize);
assertTrue("this catalog never ends!!!", i<100);
}
}
/**
* A user shall be able to purchase a product in the catalog.
*/
@Test
public void purchaseProduct() {
log.info("*** purchaseProduct ***");
//the user selects a product
Product product=null;
Random random=new Random();
for (int i=0; product == null; i++) {
List<Product> products=catalog.getProducts(random.nextInt(100), 1);
product=products.iterator().next();
assertTrue("I can't find anything to buy!!!", i<1000);
}
//the user adds the product to their shopping cart by providing the
//product id and their credentials
int count=catalog.addToCart(product.getId(), validEmail);
//the user receives a count of the items in the cart
assertEquals("somebody tweeked my cart!!!!", 1, count);
//the user checks out with the cashier -- payment not yet implemented
double total=purchasing.checkout(validEmail, validPassword);
//the user gets a total amount back as their receipt
assertEquals("price doesn't add up", product.getPrice(), total, .01);
}
|-- pom.xml `-- src |-- main | |-- java | | `-- ejava | | `-- examples | | `-- blpurchase | | |-- bo | | | |-- Account.java | | | |-- Cart.java | | | `-- Product.java | | |-- bl | | | |-- Catalog.java | | | `-- Purchasing.java | | `-- blimpl | | |-- CatalogImpl.java | | `-- PurchasingImpl.java | `-- resources `-- test |-- java | `-- ejava | `-- examples | `-- blpurchase | `-- bl | |-- PurchaseTest.java | `-- PurchasingFactory.java `-- resources |-- hibernate.properties |-- log4j.xml `-- META-INF `-- persistence.xml
Copyright © 2019 jim stafford (jim.stafford@jhu.edu)
Built on: 2019-08-22 07:09 EST
Abstract
This presentation provides and introduction to the Data Access Object (DAO) pattern to encapsulate access to data, database schema used to design data organization in RDBMS systems, and SQL to access the data.
Table of Contents
At the completion of this topic, the student shall
have an understanding of:
The role played by a DAO and business tier as it relates to accessing data
Core concepts to relational tables
How to define database tables and relationships using RDBMS schema
How to access data within the database using SQL
be able to:
Design the interface for a DAO
Create a schema in the database
Implement a basic DAO using EntityManager and native SQL commands
Interfaces to data sources vary
Relational Database Management Systems (RDBMS)
NoSQL Solutions
Flat Files
Backend Systems
Even standard RDBMS/SQL interfaces can vary
Many components within application need access to data
Interfaces to data vary by technology and vendor
Least common denominator option for portability may not be feasible in all cases
May make use of vendor extensions
Impact of unique interfaces significant when exposed to many components and component types
Components need more abstraction and shielding from the details of the persistent store
Use a Data Access Object (DAO) to abstract and encapsulate access to business objects in the data source
Business Logic
Object within the business domain that needs access to data (e.g., session bean)
Knows when/why data is needed, but not where or how to access it
Data Access Object
Abstracts the access details from the business object
Knows where/how data is accessed, but not when or why to access it
Business Object (Entity)
An entity within the business logic
Encapsulates information and data business rules within application
A data carrier of information to/from the DAO
Data Source
Physically stores the data (e.g., database)
Key point: hide details from business logic and other interfacing components
Centralizes All Data Access into a Separate Layer
Easier to maintain
Enables Transparency
Access to implementation details hidden within DAO
Enables Easier Migration
Client layers encapsulated from changes
Reduces Code Complexity in Business Logic
No details, such as SQL, in business logic
Was hard to abstract with EJB 2.x Container Managed Persistence (CMP) frameworks
EJB3 Java Persistence API provides a significant amount of abstraction
Technology agnostic and business object-focused
No mention of Connection or EntityManager in methods
Ability to at least CRUD (with possible options)
Aggregate data functions added when behavior better performed at data source
Extensions added to address data access details for specific use cases (e.g., LAZY/EAGER load)
...
import javax.persistence.PersistenceException;
public interface BookDAO {
Book create(Book book) throws PersistenceException;
Book update(Book book) throws PersistenceException;
Book get(long id) throws PersistenceException;
void remove(Book book) throws PersistenceException;
List<Book> findAll(int start, int count) throws PersistenceException;
}
The declaration of the unchecked/runtime exception PersistenceException is not required and is only being done here for extra clarity
Runtime Exceptions
Used to report unexpected issues (e.g., no connection)
Extends java.lang.RuntimeException
ex. javax.persistence.PersistenceException
Checked Exceptions
Used to report anticipated errors mostly having to do with input
Extends java.lang.Exception
Adds implementation out-of-band from DAO interface
...
public class JPABookDAOImpl implements BookDAO {
private EntityManager em;
public void setEntityManager(EntityManager em) {
this.em = em;
}
@Override
public Book create(Book book) { ... }
@Override
public Book update(Book book) { ... }
@Override
public Book get(long id) { ... }
@Override
public void remove(Book book) { ... }
@Override
public List<Book> findAll(int offset, int limit) { ... }
}
Demonstrates how technology-neutral DAO clients can be when dao implementation is injected into client.
...
public class BookDAOTestBase {
protected BookDAO dao; //sub-classes will provide an implementation.
protected Book makeBook() {
Random random = new Random();
Book book = new Book();
book.setTitle("GWW-" + random.nextInt());
...
return book;
}
@Test
public void testCreate() {
Book book = makeBook();
assertEquals("id not assigned", 0, book.getId());
book = dao.create(book);
assertTrue("id not assigned", book.getId()>0);
}
@Test
public void testGet() {... }
@Test
public void testUpdate() { ... }
@Test
public void testDelete() { ... }
@Test
public void testFindAll() { ... }
}
Business Objects represent too much information or behavior to transfer to remote client
Client may get information they don't need
Client may get information they can't handle
Client may get information they are not authorized to use
Client may get too much information/behavior to be useful (e.g., entire database serialized to client)
Some clients are local and can share object references with business logic
Handling specifics of remote clients outside of core scope of business logic
Layer a Remote Facade over Business Logic
Remote Facade constructs Data Transfer Objects (DTOs) from Business Objects that are appropriate for remote client view
Remote Facade uses DTOs to construct or locate Business Objects to communicate with Business Logic
Data Transfer Object
Represents a subset of the state of the application at a point in time
Not dependent on Business Objects or server-side technologies
Doing so would require sending Business Objects to client
XML and JSON provide the “ultimate isolation” in DTO implementation/isolation
Remote Facade
Uses Business Logic to perform core business logic
Layered on top of Business Logic to translate between Business Objects and DTOs
Business Logic
Continues to perform core duties as described in DAO Pattern
Business Object (Entity)
Continues to perform core duties as described in DAO Pattern
May have more server-side-specific logic when DTOs are present in the design
Clients only get what they need
Clients only get what they understand
Clients only get what they are authorized to use
Remote and Local interfaces to services are different
Makes it harder to provide location transparency
Lightweight Business Objects can be used as DTOs
Remote Facade must make sure they are “pruned” of excess related items before transferring to client
Remote Facade must make sure they are “cleaned” of DAO persistence classes before transferring to client
DBMS based on a relational model
Introduced by E. F. Codd in 1970s
Some challenges by other forms but still remains a standard for corporate data stores
Table
Group of columns
Represents a type
Commonly mapped to a Java class
Column
Single piece of data
Represents a property
Commonly mapped to a Java class attribute
Sample (H2) Column Types[7]
INTEGER
DECIMAL
TIME
DATE
TIMESTAMP
VARCHAR
BLOB
CLOB
NOT NULL
Row cannot exist without this column value supplied
UNIQUE
No other row may have a column with this value
FOREIGN KEY
If supplied, must reference matching column(s) of existing row
Foreign Key Join
Column within child table references parent
Primary Key Join
Foreign key column within child table is child's primary key column. Parent and child table primary keys must match.
Link Table Join
Foreign keys to parent/child expressed in separate table
Foreign keys may be defined in child table or link table
Link tables can always be used (at an extra cost) no matter the cardinality
Foreign keys cannot be defined on "one side" of a one-to-many relationship
Link table must be used if foreign key cannot be placed on many side
Link tables must be used in many-to-many relationships
Used to manipulate schema in RDBMS
create table JPADAO_AUTHOR (
ID integer generated by default as identity,
FIRST_NAME varchar(16) not null,
LAST_NAME varchar(32) not null,
primary key (ID)
);
create table JPADAO_BOOK (
ID bigint generated by default as identity,
DESCRIPTION varchar(1000),
PAGES integer,
TITLE varchar(32) not null,
AUTHOR_ID integer,
primary key (ID)
);
alter table JPADAO_BOOK
add constraint JPADAO_BOOK_AUTHOR_FK
foreign key (AUTHOR_ID)
references JPADAO_AUTHOR
create index JPADAO_BOOK_AUTHOR_FKX on JPADAO_BOOK(AUTHOR_ID);
create unique index JPADAO_BOOK_TITLE_IDX on JPADAO_BOOK(TITLE);
src/main/resources/ `-- ddl |-- book-create.ddl |-- book-drop.ddl |-- book-tuningadd.ddl `-- book-tuningremove.ddl target/classes/ `-- ddl |-- book-create.ddl |-- book-drop.ddl |-- book-tuningadd.ddl `-- book-tuningremove.ddl
insert
into
JPADAO_BOOK
(ID, DESCRIPTION, PAGES, TITLE)
values
(null, 'this and that', 1037, 'gww')
Get the generated primary key value
call IDENTITY()
public class JDBCBookDAOImpl implements BookDAO {
private Connection connection;
public void setConnection(Connection connection) {
this.connection = connection;
}
@Override
public Book create(Book book) throws PersistenceException {
try (PreparedStatement insertStatement=getInsertPreparedStatement(connection, book);
PreparedStatement idStatement=getIdentityStatement(connection)){
insertStatement.execute();
try (ResultSet rs = idStatement.executeQuery()) {
if (rs.next()) {
Field id = Book.class.getDeclaredField("id");
id.setAccessible(true);
id.set(book, rs.getLong(1));
} else {
throw new PersistenceException("no identity returned from database");
}
} catch (NoSuchFieldException ex) {
throw new PersistenceException("Error locating id field", ex);
} catch (IllegalAccessException ex) {
throw new PersistenceException("Access error setting id", ex);
}
return book;
} catch (SQLException ex) {
throw new PersistenceException("SQL error creating book", ex);
}
}
...
}
private PreparedStatement getInsertPreparedStatement(Connection c, Book book) throws SQLException {
PreparedStatement statement=connection.prepareStatement(
"insert into JPADAO_BOOK (ID, DESCRIPTION, PAGES, TITLE) " +
"values (null, ?, ?, ?)");
statement.setString(1, book.getDescription());
statement.setInt(2, book.getPages());
statement.setString(3, book.getTitle());
return statement;
}
private PreparedStatement getIdentityStatement(Connection c) throws SQLException {
PreparedStatement statement = connection.prepareStatement("call identity()");
return statement;
}
public class JPANativeSQLBookDAO implements BookDAO {
private EntityManager em;
public void setEntityManager(EntityManager em) {
this.em = em;
}
@Override
public Book create(Book book) throws PersistenceException {
em.createNativeQuery(
"insert into JPADAO_BOOK (ID, DESCRIPTION, PAGES, TITLE) " +
"values (null, ?1, ?2, ?3)")
.setParameter(1, book.getDescription())
.setParameter(2, book.getPages())
.setParameter(3, book.getTitle())
.executeUpdate();
int idVal = ((Number)em.createNativeQuery("call identity()")
.getSingleResult()).intValue();
try {
Field id = Book.class.getDeclaredField("id");
id.setAccessible(true);
id.set(book, idVal);
} catch (Exception ex) {
throw new RuntimeException("Error setting id", ex);
}
return book;
}
...
}
Table of Contents
At the completion of this topic, the student shall
have an understanding of:
Requirements for entity classes
How to define a persistence unit
How to obtain an entity manager
Persistence methods available from EntityManager
be able to:
Define a Java class as an entity
Define a persistence unit with entity class(es)
Implement DAO operations in terms of javax.persistence.Persistence API
Earlier versions of EJB Spec defined persistence as part of javax.ejb.EntityBean
JavaEE 5 moved persistence to its own specification
Java Persistence API (JPA) version 1.0
javax.persistence
Ease of use API above JDBC
Fueled by the addition of @Annotations to the JavaSE language
Provides
Object/Relational Mapping (ORM) Engine
Query Language (JPA-QL) - SQL-like - main carryover from EJB 2.x
JavaEE 6 advanced specification
Java Persistence API (JPA) version 2.0
More mapping capabilities
More entity manager capabilities
Standardization of properties
Criteria API
JavaEE 7 enhanced specification
Java Persistence API (JPA) version 2.1
Converters - to convert attributes to/from DB types
Criteria API Bulk Updates - no longer tied to just queries
Stored Procedure Query support
Partial fetching of objects
...
JavaEE 8
Java Persistence API (JPA) version 2.2
Streams - to fetch data from database in a stream versus batch
Date and Time API Support - eliminates need for converter
@Repeatable - no longer have to wrap repeated annotations within single wrapper
...
Replaced EJB 2.x Home Functionality
Handles O/R Mapping to the database
Provides APIs for
Inserting into database
Updating entities in database
Finding and querying for entities in database
Removing entities from database
Provides caching
Integrates with JTA transactions when on server-side
Tightly integrated with JavaEE and EJB, but not coupled to it
Plain Old Java Objects (POJOs)
Nothing special happens when calling new()
Author author = new Author();
From JPA perspective the above is a new/unmanaged entity
Entity minimum requirements:
@javax.persistence.Entity
public class Author {
@javax.persistence.Id
private long id;
private String firstName;
private String lastName;
private String subject;
private Date publishDate;
public Author() {
}
...
}
Annotated as an Entity or declared in an orm.xml
Unique identity (form primary key(s))
Public default constructor
Persistent when associated with an entity manager/persistence context
em.persist(author);
Author author = new Author();
author.setFirstName("dr");
author.setLastName("seuss");
author.setSubject("children");
author.setPublishDate(new Date());
logger.debug("creating author: {}", author);
assertEquals("unexpected initialized id", 0, author.getId());
logger.debug("em.contains(author)={}", em.contains(author));
assertFalse("author managed", em.contains(author));
em.persist(author);
logger.debug("created author: {}", author);
logger.debug("em.contains(author)={}", em.contains(author));
assertNotEquals("missing id", 0, author.getId());
assertTrue("author not managed", em.contains(author));
-creating author:ejava.examples.daoex.bo.Author@1d8e9e, id=0, fn=dr, ln=seuss, subject=children, pdate=Mon Sep 17 00:22:25 EDT 2012, version=0 -em.contains(author)=false -created author:ejava.examples.daoex.bo.Author@1d8e9e, id=50, fn=dr, ln=seuss, subject=children, pdate=Mon Sep 17 00:22:25 EDT 2012, version=0 -em.contains(author)=true
Associated with persistence context
Has identity
Changes to the entity will impact the database
Method em.contains(entity)
returns true
Has identity but not associated with persistence context
Changes to entity will not impact the database
Method em.contains(entity)
returns false
An entity becomes detached when:
Has not yet been persisted
After a transaction-scoped transaction is committed
After a transaction rollback <-- remember this in your error logic
Manually detaching entity from persistence context thru em.detach()
Manually clearing the persistence context thru em.clear()
Closing EntityManager
Serializing entity thru a remote interface
A set of managed instances managed by an EntityManager
All entities become detached once closed
Two types:
Extended
Author author = new Author();
...
em.persist(author);
em.getTransaction().begin();
em.getTransaction().commit();
author.setFirstName("foo");
em.getTransaction().begin();
em.getTransaction().commit();
em.getTransaction().begin();
author.setFirstName("bar");
em.getTransaction().commit();
Live beyond a single transaction
Allow long-lived algorithms to process without tying up a database transaction
Transaction-Scoped
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Author createAuthor(...) {
Author author = new Author();
...
em.persist(author);
return author;
}
Begin/end at transaction boundaries
Injected by containers
A set of classes that are mapped to the database
Defined in META-INF/persistence.xml
Entity classes may be named in persistence.xml or searched for
Entity mapping may be provided, augmented, or overridden with orm.xml
mapping file
|-- ejava | `-- examples | `-- daoex | |-- bo | | `-- Author.class | |-- dao | | |-- AuthorDAO.class | | `-- DAOException.class | `-- jpa | `-- JPAAuthorDAO.class `-- META-INF |-- orm.xml `-- persistence.xml
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="jpaDemo">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<mapping-file>META-INF/orm.xml</mapping-file>
<properties>
<!-- standard properties -->
<property name="javax.persistence.jdbc.url" value="jdbc:h2:./target/h2db/ejava"/>
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<!-- hibernate-specific properties -->
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<!-- set to 0 to improve error messages when needed
<property name="hibernate.jdbc.batch_size" value="0"/>
-->
</properties>
</persistence-unit>
</persistence>
The above example:
defines properties for EntityManager to establish physical connections to database
this form of connection management only occurs for 2-tier thin clients (e.g., unit tests)
not a good place for potentially changing or sensitive property values
We can alternatively supply and override all the properties from the META-INF/persistence.xml at runtime if we control the creation of the EntityManagerFactory
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="ejbsessionbank">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
<jar-file>lib/info.ejava.examples.ejb-ejbsessionBankImpl-5.0.0-SNAPSHOT.jar</jar-file>
<properties>
<property name="hibernate.dialect"
value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.show_sql" value="false"/>
<!-- create is used here for demo project only -->
<property name="hibernate.hbm2ddl.auto" value="create"/>
<!--
<property name="hibernate.jdbc.batch_size" value="0"/>
-->
</properties>
</persistence-unit>
</persistence>
The above example:
uses a DataSource from JNDI tree to obtain connections to database
references entity classes in a separate archive through a relative path using jar-file
supplies references to resources that are fairly stable in value and non-sensitive
Can be used to specify connection properties outside of persistence.xml
Useful in separating production mapping information from runtime connection properties
#hibernate-specific alternate source of persistence.xml properties hibernate.connection.url=jdbc:h2:./target/h2db/ejava hibernate.connection.driver_class=org.h2.Driver hibernate.connection.password= hibernate.connection.username=sa hibernate.dialect=org.hibernate.dialect.H2Dialect hibernate.show_sql=true hibernate.format_sql=true #hibernate.jdbc.batch_size=0
Even though one can specify properties within the persistence.xml#persistence-unit or hibernate.properties file, they are not always the same. If you use the hibernate.properties file to specify the url, driver, and credentials -- always use the hibernate form of the name to avoid stumbling on the cases where hibernate fails to look for the javax.persitence form of the name for properties coming from the hibernate.properties file.
referenced by the persistence.xml
supplies mapping metadata for the entities that are either not annotated or overriding the annotations
each entity or group of entities within the same persistence unit can be defined in separate files
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd"
version="2.1">
<entity class="ejava.examples.daoex.bo.Author"
access="FIELD"
metadata-complete="false"
name="jpaAuthor">
<table name="DAO_AUTHOR"/>
<attributes>
<id name="id">
<generated-value strategy="SEQUENCE"
generator="AUTHOR_SEQUENCE"/>
</id>
</attributes>
</entity>
</entity-mappings>
name
Identity used to reference persistence unit
provider
Fully qualified name of javax.persistence.PersistenceProvider
Not needed if provider is in classpath
mapping-file
Path reference to an orm.xml
mapping file
jta-data-source
JNDI path of a JTA javax.sql.DataSource
non-jta-datasource
JNDI path of a RESOURCE_LOCAL javax.sql.DataSource
jarfile
Reference to an archive with entity classes
class
Fully qualified package name of entity class
One source of entity information
exclude-unlisted-classes
If set, provider will not scan to discover entity classes
properties
name/value property pairs to express additional configuration info
Database schema generation
Live database used by EntityManager has been prepared for persistence at runtime
Useful for demos and tutorials
Not an option for production
Script schema generation
Database schema commands written without changing the database
Useful in debugging "what does the provider think I am modeling?"
Good starting source script to author database migration scripts from
Vendor-specific option
Persistence provider is required to provide schema-generation capability
Likely has for years through a proprietary interface
javax.perisistance option
Standard interface added in JPA 2.1 for requesting schema generation
hibernate.hbm2ddl.auto
Controls hibernate schema generation
create, drop, create-drop, update, validate, and none
hibernate.hbm2ddl.import_files
Allows for self-authored files to be used as well
Expressed thru hibernate.properties using hibernate.hbm2ddl properties
hibernate.hbm2ddl.auto=create-drop
hibernate.hbm2ddl.import_files=/ddl/mydb-tuningdrop.ddl,/ddl/mydb-tuning.ddl
hibernate.connection.url=${jdbc.url}
hibernate.connection.driver_class=${jdbc.driver}
hibernate.connection.username=${jdbc.user}
hibernate.connection.password=${jdbc.password}
#hibernate.show_sql=true
#hibernate.format_sql=true
Can be expressed thru persistence.xml but use standard properties instead to avoid confusion
javax.persistence.schema-generation.database.action
Values: drop-and-create, drop, create, none
javax.persistence.schema-generation.create-source
Values: metadata (default), script, metadata-then-script, script-then-metadata
javax.persistence.schema-generation.create-script-source - provides reference to create source
javax.persistence.schema-generation.drop-script-source - provides reference to drop source
javax.persistence.sql-load-script-source
Provides reference to script to execute after schema initialized
Useful in Pre-populating sample database tables with content
Database schema generation example
<persistence-unit name="jpa-schemagen-test">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<properties>
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
<!-- a database connection definition is required for database actions -->
<property name="javax.persistence.jdbc.url" value="${jdbc.url}"/>
<property name="javax.persistence.jdbc.driver" value="${jdbc.driver}"/>
<property name="javax.persistence.jdbc.user" value="${jdbc.user}"/>
<property name="javax.persistence.jdbc.password" value="${jdbc.password}"/>
</properties>
</persistence-unit>
Script schema generation example
<persistence-unit name="jpa-schemagen-test">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<properties>
<!-- a script file containing create and drop commands will be generated without interacting with database -->
<property name="javax.persistence.schema-generation.scripts.action" value="drop-and-create"/>
<property name="javax.persistence.schema-generation.scripts.create-target" value="${project.build.outputDirectory}/ddl/${project.artifactId}-create.ddl"/>
<property name="javax.persistence.schema-generation.scripts.drop-target" value="${project.build.outputDirectory}/ddl/${project.artifactId}-drop.ddl"/>
<!-- otherwise we would get 1 line per statement without standard delimiter character -->
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.hbm2ddl.delimiter" value=";"/>
<!-- required when no database connection specified -->
<property name="hibernate.dialect" value="${hibernate.dialect}"/>
</properties>
</persistence-unit>
Scripts written to target files
target/classes/ddl/ |-- jpa-schemagen-create.ddl `-- jpa-schemagen-drop.ddl
Create script contains commands to create schema
create sequence hibernate_sequence start with 1 increment by 1;
create table JPAUTIL_TABLET (
id integer not null,
maker varchar(255),
primary key (id)
);
Drop script contains commands to drop schema
drop table JPAUTIL_TABLET if exists;
drop sequence if exists hibernate_sequence;
Test Setup -- These actions occur outside of the individual @Test methods and normally not within the business and DAO logic. This is the kind of activity the server-side container will primarily take care of.
Create single EntityManagerFactory to share across multiple EntityManagers
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public class MyJPATest {
private static EntityManagerFactory emf;
private EntityManager em;
@BeforeClass
public static void setUpClass() {
emf = Persistence.createEntityManagerFactory("jpaDemo");
}
Create EntityManager before each test
@Before
public void setUp() throws Exception {
em = emf.createEntityManager();
}
Runtime -- These actions occur either within the @Test or in the DAO under test. However, business and DAO logic rarely interact with the transaction calls. Transaction control is the job of the container and under the configuration of the server-side EJB. Think of a JUnit TestCase and @Test as being a simulated replacement for the container and EJB configuration so that we can test the business and DAO logic.
Start Transaction - this is the role of the container
em.getTransaction().begin();
Interact with EntityManager - this is the role of the DAO
em.persist(author);
Commit or Rollback Transaction - this is the role of the container
em.getTransaction().commit();
-or-
em.getTransaction().rollback();
Clean up
Close EntityManager after each test
@After
public void tearDown() throws Exception {
if (em != null) {
em.close();
em=null;
}
}
Close EntityManagerFactory after all tests
@AfterClass
public static void tearDownClass() {
if (emf != null) {
emf.close();
emf=null;
}
}
It is always good to try to keep @After or @AfterClass lifecycle methods from throwing sloppy exceptions (e.g., NullPointerException). These lifecycle methods get called whether the test is passing, failing, or even failed to fully initialize. If the @After or @AfterClass lifecycle methods throw an exception during cleanup after an exception -- their error will mask the real error and add confusion to the test results. Start each of these methods with a check of whether the sibling @BeforeClass or @Before lifecycle methods initialized what this method is trying to shutdown.
public interface javax.persistence.EntityManager{ ... }
void persist(Object entity);
<T> T find(Class<T> entityClass, Object primaryKey);
<T> T find(Class<T> entityClass, Object primaryKey, Map<String, Object> properties);
<T> T merge(T entity);
void remove(Object entity);
<T> T getReference(Class<T> entityClass, Object primaryKey);
void clear();
void detach(Object entity);
boolean contains(Object entity);
void flush();
void setFlushMode(javax.persistence.FlushModeType);
javax.persistence.FlushModeType getFlushMode();
void refresh(Object);
void refresh(Object, java.util.Map);
void lock(Object entity, javax.persistence.LockModeType);
void lock(Object entity, javax.persistence.LockModeType, Map<String, Object> properties);
<T> T find(Class<T> entityClass, Object primaryKey, javax.persistence.LockModeType);
<T> T find(Class<T> entityClass, Object primaryKey, javax.persistence.LockModeType, Map<String, Object> properties);
void refresh(Object, javax.persistence.LockModeType);
void refresh(Object, javax.persistence.LockModeType, Map<String, Object> properties);
javax.persistence.LockModeType getLockMode(Object entity);
javax.persistence.Query createQuery(String jpaql);
<T> javax.persistence.TypedQuery<T> createQuery(String jpaql, Class<T> resultClass);
javax.persistence.Query createNamedQuery(String name);
<T> javax.persistence.TypedQuery<T> createNamedQuery(String name, Class<T> resultClass);
javax.persistence.Query createNativeQuery(String sql);
javax.persistence.Query createNativeQuery(String sql, Class resultClass);
javax.persistence.Query createNativeQuery(String sql, String resultMapping);
<T> javax.persistence.TypedQuery<T> createQuery(javax.persistence.criteria.CriteriaQuery<T> criteria);
javax.persistence.criteria.CriteriaBuilder getCriteriaBuilder();
void close();
boolean isOpen();
javax.persistence.EntityTransaction getTransaction();
void joinTransaction();
void setProperty(String key, Object value);
java.util.Map getProperties();
<T> T unwrap(Class<T> clazz);
Object getDelegate();
javax.persistence.metamodel.Metamodel getMetamodel();
javax.persistence.EntityManagerFactory getEntityManagerFactory();
Author author = new Author();
author.setFirstName("dr");
author.setLastName("seuss");
author.setSubject("children");
author.setPublishDate(new Date());
logger.debug("creating author: {}", author);
em.persist(author);
logger.debug("created author: {}", author);
-creating author:ejava.examples.daoex.bo.Author@17e7691, id=0, fn=dr, ln=seuss, subject=children, pdate=Sun Sep 16 10:14:32 EDT 2012, version=0 -created author:ejava.examples.daoex.bo.Author@17e7691, id=50, fn=dr, ln=seuss, subject=children, pdate=Sun Sep 16 10:14:32 EDT 2012, version=0
Inserts entity into database
Actual insert time depends on transaction active and FlushMode
Extended Persistence Context - queues insert until transaction active
Transaction-Scoped Persistence Context - always has transaction active
Flush occurs automatically prior to or during commit()
Flush can be forced with manual em.flush()
call
New or removed entity enters managed state
All further changes are watched and will update database
logger.debug("em.contains(author)={}", em.contains(author));
em.persist(author);
logger.debug("created author: {}", author);
logger.debug("em.contains(author)={}", em.contains(author));
-em.contains(author)=false
-created author:...
-em.contains(author)=true
Existing entity is ignored
Cascades will still occur for CascadeType.PERSIST and ALL relationships
em.persist(author);
...
em.persist(author);
It is quite legal to call persist() with an already managed entity. This is how one can get cascade actions to be applied to new relationships formed after the entity is originally managed.
Detached entities will be rejected
Entities with an identity but not associated with a persistence context
Author author = new Author(1);
author.setFirstName("dr");
...
logger.debug("creating author: {}", author);
logger.debug("em.contains(author)={}", em.contains(author));
try {
em.persist(author);
fail("did not detect detached entity");
} catch (PersistenceException ex) {
logger.debug("caught expected exception:" + ex.getLocalizedMessage(), ex);
}
logger.debug("em.contains(author)={}", em.contains(author));
-creating author:ejava.examples.daoex.bo.Author@ad339b, id=1, ... -em.contains(author)=false -caught expected exception:javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: ejava.examples.daoex.bo.Author -em.contains(author)=false
<T> T find(Class<T> entityClass, Object primaryKey);
<T> T find(Class<T> entityClass, Object primaryKey, Map<String, Object> properties);
Searches for the entity by primary key value
Returns managed entity if found, else null
Properties permitted for vendor-specific values and ignored if not understood
<T> T getReference(Class<T> entityClass, Object primaryKey);
Searches for the entity by primary key
Returns a LAZY load reference if found
Throws EntityNotFoundException if not exist
//create initial author
Author author = new Author();
...
em.persist(author);
//create detached author with changes
Author author2 = new Author(author.getId());
author2.setFirstName("updated " + author.getFirstName());
...
//merge changes
Author tmp = em.merge(author2);
em.getTransaction().begin();
em.getTransaction().commit();
...
//verify results
assertFalse("author2 is managed", em.contains(author2));
assertTrue("tmp Author is not managed", em.contains(tmp));
assertSame("merged result not existing managed", author, tmp);
...
//verify changes were made to the DB
Author author3 = em.find(Author.class, author.getId());
assertEquals("updated " + firstName, author3.getFirstName());
Update database with state of detached object
State of detached object copied into managed entity or new instance created
Merging a managed entity is ignored. CascadeType.MERGE and ALL relationships are propagated
Merging a removed entity instance results in an IllegalArgumentException
Includes automatic checking of @Version
property
Could be replaced with manual merge
public Author update(Author author) {
Author dbAuthor = em.find(Author.class, author.getId());
dbAuthor.setFirstName(author.getFirstName());
dbAuthor.setLastName(author.getLastName());
dbAuthor.setSubject(author.getSubject());
dbAuthor.setPublishDate(author.getPublishDate());
return dbAuthor;
}
The above manual merge will successfully merge a transient entity -- but what errors and conditions does it not account for?
void remove(Object entity);
Managed entity is removed from database
Actual delete time depends on transaction active and FlushMode
Extended Persistence Context - queues delete until transaction active
New entities are ignored but CascadeType.REMOVE and ALL relationships are cascaded
logger.debug("em.contains(author)={}", em.contains(author));
em.remove(author);
logger.debug("em.contains(author)={}", em.contains(author));
-em.contains(author)=false -HHH000114: Handling transient entity in delete processing -em.contains(author)=false
Detached entities are rejected
Author author = new Author(1);
...
try {
em.remove(author);
fail("did not reject removal of detached object");
} catch (IllegalArgumentException ex) {
logger.debug("caught expected exception:" + ex);
}
-caught expected exception:java.lang.IllegalArgumentException: Removing a detached instance ejava.examples.daoex.bo.Author#1
Removed entities are ignored
Author author = new Author();
...
logger.debug("peristed: {}", author);
logger.debug("em.contains(author)={}", em.contains(author));
em.remove(author);
logger.debug("em.contains(author)={}", em.contains(author));
//entity managers will ignore the removal of a removed entity
em.remove(author);
logger.debug("em.contains(author)={}", em.contains(author));
-peristed:ejava.examples.daoex.bo.Author@6c5356, id=50, ... -em.contains(author)=true -em.contains(author)=false -em.contains(author)=false
boolean contains(Object entity);
Returns true if object is managed in the persistence context
void clear();
Clears all entities and queued changes from the persistence context
All entities become detached
em.persist(author);
//callers can detach entity from persistence context
logger.debug("em.contains(author)={}", em.contains(author));
logger.debug("detaching author");
em.getTransaction().begin();
em.flush();
em.detach(author);
logger.debug("em.contains(author)={}", em.contains(author));
em.getTransaction().commit();
//changes to detached entities do not change database
author.setFirstName("foo");
em.getTransaction().begin();
em.getTransaction().commit();
Author author2 = em.find(Author.class, author.getId());
logger.debug("author.firstName={}", author.getFirstName());
logger.debug("author2.firstName={}", author2.getFirstName());
assertNotEquals("unexpected name change", author.getFirstName(), author2.getFirstName());
-em.contains(author)=true -detaching author -em.contains(author)=false -author.firstName=foo -author2.firstName=dr
Detaches existing entity from persistence context
Detach cascaded to CascadeType.DETACH and ALL relationships
Subsequent changes to entity will not change database
Portable use requires call to flush() prior to detach()
New entities are ignored
Author author = new Author();
logger.debug("em.contains(author)={}", em.contains(author));
logger.debug("detaching author");
em.detach(author);
logger.debug("em.contains(author)={}", em.contains(author));
-em.contains(author)=false -detaching author -em.contains(author)=false
Detached entities are ignored
Author author = new Author();
...
em.persist(author);
em.getTransaction().begin();
em.getTransaction().commit();
//detaching detached entity will be ignored
Author detached = new Author(author.getId());
logger.debug("em.contains(author)={}", em.contains(detached));
logger.debug("detaching detached author");
em.detach(detached);
logger.debug("em.contains(author)={}", em.contains(detached));
-em.contains(author)=false -detaching detached author -em.contains(author)=false
void flush();
Synchronizes cached changes with underlying database
Requires active transaction
void setFlushMode(javax.persistence.FlushModeType);
javax.persistence.FlushModeType getFlushMode();
AUTO (default) - unspecified
COMMIT - flush only happens during commit
em.persist(author);
em.getTransaction().begin();
em.getTransaction().commit();
//change DB state out-of-band from the cache
em.getTransaction().begin();
String newName="foo";
int count=em.createQuery(
"update jpaAuthor a set a.firstName=:name where a.id=:id")
.setParameter("id", author.getId())
.setParameter("name", newName)
.executeUpdate();
em.getTransaction().commit();
assertEquals("unexpected count", 1, count);
//object state becomes stale when DB changed out-of-band
logger.debug("author.firstName={}", author.getFirstName());
assertNotEquals("unexpected name", newName, author.getFirstName());
//get the cached object back in sync
logger.debug("calling refresh");
em.refresh(author);
logger.debug("author.firstName=" + author.getFirstName());
assertEquals("unexpected name", newName, author.getFirstName());
-author.firstName=dr -calling refresh -author.firstName=foo
Updates/overwrites cached entity state with database state
Refresh of new entity results in java.lang.IllegalArgumentException
Author author = new Author();
author.setFirstName("test");
author.setLastName("new");
try {
em.refresh(author);
fail("refresh of new entity not detected");
} catch (IllegalArgumentException ex) {
logger.debug("caught expected exception:" + ex);
}
-caught expected exception:java.lang.IllegalArgumentException: Entity not managed
Refresh of detached entity results in java.lang.IllegalArgumentException
em.persist(author);
em.getTransaction().begin();
em.getTransaction().commit();
//refreshing a detached entity will get rejected
Author detached = new Author(author.getId());
em.refresh(author);
logger.debug("refreshed managed entity");
try {
em.refresh(detached);
fail("refresh of detached entity not detected");
} catch (IllegalArgumentException ex) {
logger.debug("caught expected exception:" + ex);
}
-refreshed managed entity -caught expected exception:java.lang.IllegalArgumentException: Entity not managed
Optimistic
Entity assumed not to have concurrent access
No active database locks are obtained at start
Success judged based on entity state at end
State tracked in entity @Version
field
Pessimistic
Entity requires mitigation for concurrent access
Active database locks are obtained at start of transaction
NONE
No locks
OPTIMISTIC (was READ)
Requires entity to have a @Version
property
Prevent dirty reads
Prevent non-repeatable reads
OPTIMISTIC_FORCE_INCREMENT (was WRITE)
Requires entity to have a @Version
property
Update only occurs if change has proper version
Version is incremented upon update
Incorrect version results in OptimisticLockException
PESSIMISTIC_READ
Supported with or without @Version
property
Obtains active database lock
Provides repeatable reads
Does not block other reads
PESSIMISTIC_WRITE
Supported with or without @Version
property
Forces serialization of entity updates among transactions
PESSIMISTIC_FORCE_INCREMENT
Requires entity to have a @Version
property
PESSIMISTIC_WRITE lock with increment of @Version
void lock(Object entity, javax.persistence.LockModeType);
void lock(Object entity, javax.persistence.LockModeType, Map<String, Object> properties);
Requests lock on entity
<T> T find(Class<T> entityClass, Object primaryKey, javax.persistence.LockModeType);
<T> T find(Class<T> entityClass, Object primaryKey, javax.persistence.LockModeType, Map<String, Object> properties);
Find object by primary key and lock
void refresh(Object, javax.persistence.LockModeType);
void refresh(Object, javax.persistence.LockModeType, Map<String, Object> properties);
Refresh entity state and obtain lock
javax.persistence.Query createQuery(String jpaql);
<T> javax.persistence.TypedQuery<T> createQuery(String jpaql, Class<T> resultClass);
javax.persistence.Query createNamedQuery(String name);
<T> javax.persistence.TypedQuery<T> createNamedQuery(String name, Class<T> resultClass);
Create query based on JPA Query Language (JPAQL)
javax.persistence.Query createNativeQuery(String sql);
javax.persistence.Query createNativeQuery(String sql, Class resultClass);
javax.persistence.Query createNativeQuery(String sql, String resultMapping);
Create query based on SQL
javax.persistence.EntityTransaction getTransaction();
Returns transaction object for inspection and control
Invalid call for transaction-scoped persistence contexts
void joinTransaction();
Associate a persistence context with a JTA transaction
<T> T unwrap(Class<T> clazz);
Return object of specified type
Provides access to underlying implementation classes
javax.persistence.metamodel.Metamodel getMetamodel();
Returns metamodel object for persistence context
javax.persistence.EntityManagerFactory getEntityManagerFactory();
Returns EntityManagerFactory associated with EntityManager
src |-- main | |-- java | | `-- ejava | | `-- examples | | `-- daoex | | |-- bo | | | `-- Author.java | | |-- dao | | | |-- AuthorDAO.java | | | `-- DAOException.java | | `-- jpa | | `-- JPAAuthorDAO.java | `-- resources | `-- META-INF | |-- orm.xml | `-- persistence.xml (could be placed in src/test branch) `-- test |-- java | `-- ejava | `-- examples | `-- daoex | `-- jpa | |-- JPAAuthorDAOTest.java | |-- JPACRUDTest.java | |-- JPAExtendedOnlyTest.java | |-- JPAMembershipTest.java | |-- JPASyncTest.java | `-- JPATestBase.java `-- resources |-- hibernate.properties (optional) `-- log4j.xml
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.3.1.Final</version>
<scope>test</scope>
</dependency>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
</testResource>
</testResources>
...
</build>
Above will depend on whether you use src/main or src/test for resource file(s)
<properties>
<!-- standard properties -->
<property name="javax.persistence.jdbc.url" value="${jdbc.url}"/>
<property name="javax.persistence.jdbc.driver" value="${jdbc.driver}"/>
<property name="javax.persistence.jdbc.user" value="${jdbc.user}"/>
<property name="javax.persistence.jdbc.password" value="${jdbc.password}"/>
<!-- hibernate-specific properties -->
<property name="hibernate.dialect" value="${hibernate.dialect}"/>
...
</properties>
<properties>
<jdbc.driver>org.h2.Driver</jdbc.driver>
<jdbc.url>jdbc:h2:${basedir}/target/h2db/ejava</jdbc.url>
<jdbc.user>sa</jdbc.user>
<jdbc.password/>
<hibernate.dialect>
org.hibernate.dialect.H2Dialect
</hibernate.dialect>
</properties>
<properties>
<!-- standard properties -->
<property name="javax.persistence.jdbc.url" value="jdbc:h2:/home/jcstaff/proj/ejava-javaee/git/jpaDAO/target/h2db/ejava"/>
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<!-- hibernate-specific properties -->
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
...
</properties>
Table of Contents
Introduce mapping a single class (void of relationships) to the database
Introduce primary key generation for synthetic key usage
Introduce composite keys for natural key usage
Introduce multi-table mappings for individual Java classes
At the completion of this topic, the student shall
have an understanding of:
Object/Relational Mapping (ORM) based on the Java Persistence API (JPA)
Class mapping strategies
Field mapping strategies
Primary key strategies
be able to:
Be able to map a single Java class and attributes to the database using class annotations and ORM descriptors
Be able to define a specific primary key strategy for their mapped class
Be able to create, get, set, and delete information from the database using class mappings
Plain Old Java Objects
Instantiated just like any other Java object
Bike bike = new Bike(1); bike.setMake("trek"); bike.setModel("2200"); bike.setSize(26);
Mapped to database schema
Interact with EntityManager to perform persistence operations
em.persist(bike); Bike bike2 = em.find(Bike.class, 1L); em.remove(bike);
Must
Have a non-private no-arg constructor
Be identified as an Entity (either using class annotations or ORM descriptors)
Have at least one (1) @Id property to be used as a primary key
Figure 46.1. Annotated Class Example
package ejava.examples.orm.core.annotated;
import javax.persistence.*; //brings in JPA Annotations
@Entity
public class Bike {
@Id //tells ORM that this property provides pk simple value
private long id;
private String make;
private String model;
private int size;
public Bike() {} //required non-private default ctor
public Bike(long id) { this.id = id; }
public long getId() {
return id;
}
public String getMake() { return make; }
public void setMake(String make) {
this.make = make;
}
...
}
Tells provider to include in persistence unit | |
Tells provider to use this property as the primary key | |
Default ctor required for provider to instantiate from DB |
Figure 46.2. Mapped Class Example
package ejava.examples.orm.core.mapped;
public class Bike {
private long id; //orm.xml file will map this field to Identity
private String make;
private String model;
private int size;
public Bike() {}
public Bike(long id) { this.id = id; }
public long getId() {
return id;
}
public String getMake() { return make; }
public void setMake(String make) {
this.make = make;
}
...
}
Must have a property to later define as a primary key | |
Must have a default ctor |
Figure 46.3. Mapped Class ORM.xml Example
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm
http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
version="2.0">
<entity class="ejava.examples.orm.core.mapped.Bike"
access="FIELD"
metadata-complete="true"
name="MappedBike">
<table name="Bike"/>
<attributes>
<id name="id"/>
</attributes>
</entity>
</entity-mappings>
Identifies class as an entity | |
Identifies @Id property for a primary key |
Figure 46.4. Mapped Class META-INF/persistence.xml Reference
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="ormCore">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<mapping-file>orm/Bike-orm.xml</mapping-file>
<class>ejava.examples.orm.core.annotated.Bike</class>
...
</persistence-unit>
</persistence>
Reference to orm.xml to map Java class(es) to database and make part of persistence unit | |
Reference to class that may/may not be within local archive but should be part of persistence unit |
Figure 46.5. Example Directory Structure
src/ |-- main | |-- java | | `-- ejava ... | | |-- annotated | | | `-- Bike.java | | `-- mapped | | `-- Bike.java | `-- resources | |-- META-INF | | `-- persistence.xml | `-- orm | `-- Bike-orm.xml `-- test `-- resources `-- hibernate.properties
Annotated class | |
Pure-POJO class mapped by orm.xml | |
persistence.xml defines reference to orm.xml | |
orm.xml defines Java class mapping to database | |
Properties file with database runtime properties |
Defaults to unqualified name of entity class (e.g., Bike)
Used to determine database table name
Bike entity -> BIKE table
create table Bike ( id bigint not null, make varchar(255), model varchar(255), size integer not null, primary key (id) )
Used to refer to entity in JPA-QL
select b from Bike b
Can be assigned optional name property
@Entity(name="AnnotatedBike")
class Bike {
<entity class="ejava.examples.orm.core.mapped.Bike"
name="MappedBike">
Table name can be specified separate from entity name
@Entity(name="AnnotatedBike")
@Table(name="COREORM_BIKE")
class Bike {
<entity class="ejava.examples.orm.core.mapped.Bike"
name="MappedBike">
<table name="COREORM_BIKE"/>
FIELD
AccessDirect access to Java variable
PROPERTY
AccessAccessed through Java set/get methods
Placement of @Id class annotation determines default choice when using annotations
Figure 46.6. Defining FIELD Access using Annotations
@Entity
public class Bike {
@Id
private long id;
private String make;
Figure 46.7. Defining PROPERTY Access using Annotations
@Entity
public class Bike {
@Id
public long getId() { return id; }
protected void setId(int id) {
this.id=id;
}
public String getMake() { return make; }
public void setMake(String make) {
this.make = make;
}
Per-property access override thru @Access
@Entity
public class Bike {
@Id
private long id;
private String make;
...
@Access(AccessType.PROPERTY)
public String getMake() { return make; }
public void setMake(String make) {
this.make = make;
}
Can be defined globally
<access>FIELD</access>
Can be defined per-class
<entity class="ejava.examples.orm.core.mapped.Bike"
access="FIELD">
Can be defined per-property
<entity class="ejava.examples.orm.core.mapped.Bike"
access="FIELD">
<attributes>
<id name="id"/>
<basic name="make" access="PROPERY">
</basic>
</attributes>
</entity>
Appropriate for quick prototypes
Most vendors provide tools to create database schema from entity classes
Generated schema will be consistent with Java class model and not optimized for any particular use
Less use of javax.persistence.* annotations because Java defaults mostly sufficient
Add more use of javax.persistence.* annotations to reach desired schema model
Common in most legacy and non-trivial environments
Most vendors provide tools to create entity classes from database schema (in addition to DAOs)
Generated entity classes will be consistent with database table design and not have any bearing on the business needs of the data tier
More use of javax.persistence.* annotations to get desired business objects mapped to desired database schema
@Entity.name - assigns entity name to entity class
@Table.name - assigns table name to entity class
@Column.name - assigns column name to entity property
Mostly used when generating schema
@Table
uniqueConstraints * - column groupings that will have a unique value
@Column
unique * - column will have unique value
nullable * - column may contain a null value/is optional
insertable - column will be included in SQL during initial insert
updatable - column will be included in SQL during follow-on updates
table - table for column in a multi-table mapping
length * - length of column for Strings
precision * - defines number of digits used for Numeric types (e.g., 100.02 is precision=5)
scale * - defines number of digits to use to the right of decimal place (e.g., 100.01 is scale=2)
columnDefinition * - custom DDL for column creation
* - Used only in schema generation
create table ORMCORE_CAR ( CAR_ID bigint not null, CAR_COST decimal(7,2), CAR_MAKE varchar(20) not null, CAR_MODEL varchar(20) not null, CAR_YEAR integer not null, primary key (CAR_ID) )
@Entity
@Table(name="ORMCORE_CAR")
public class Car {
@Id
@Column(name="CAR_ID", nullable=false)
private long id;
@Column(name="CAR_MAKE",
unique=false,
nullable=false,
insertable=true,
updatable=true,
length=20)
private String make;
@Column(name="CAR_MODEL", nullable=false, length=20)
private String model;
@Column(name="CAR_YEAR", nullable=false)
private int year;
@Column(name="CAR_COST", precision=7, scale=2)
private BigDecimal cost;
...
<entity class="ejava.examples.orm.core.mapped.Car"
access="FIELD">
<table name="ORMCORE_CAR"/>
<attributes>
<id name="id">
<column name="CAR_ID" nullable="false"/>
</id>
<basic name="make">
<column name="CAR_MAKE"
nullable="false"
insertable="true"
updatable="true"
length="20"/>
</basic>
<basic name="model">
<column name="CAR_MODEL" nullable="false" length="20"/>
</basic>
<basic name="year">
<column name="CAR_YEAR" nullable="false"/>
</basic>
<basic name="cost">
<column name="CAR_COST" precision="7" scale="2"/>
</basic>
</attributes>
</entity>
Added precision and scale only has impact for certain Java types (e.g., BigDecimal). Check the dialect for specific mappings and what SQL types they translate to in the source code.
# H2Dialect.java
registerColumnType( Types.DECIMAL, "decimal($p,$s)" );
registerColumnType( Types.NUMERIC, "decimal($p,$s)" );
registerColumnType( Types.DOUBLE, "double" );
Figure 47.1. More on Scale
//precision defined in ORM as precision=7, scale=2
car.setCost(new BigDecimal("12345.66"));
em.persist(car);
em.flush(); em.clear();
//get a fresh copy from the DB
Car car2 = em.find(Car.class, car.getId());
log.info("car.cost=" + car.getCost());
log.info("car2.cost=" + car2.getCost());
assertTrue("unexpectected value", car.getCost().equals(car2.getCost()));
//update beyond the scale values -- too many digits to right of decimal
car2.setCost(new BigDecimal("1234.666"));
em.flush(); em.clear();
Car car3 = em.find(Car.class, car.getId());
log.info("car2.cost=" + car2.getCost());
log.info("car3.cost=" + car3.getCost());
assertFalse("unexpected scale", car2.getCost().equals(car3.getCost()));
-car.cost=12345.66 //Value created within scale=2 -car2.cost=12345.66 //Value correctly retrieved from DB -car2.cost=1234.666 //Value goes beyond scale=2 -car3.cost=1234.67 //Value rounded down to scale from DB
Figure 47.2. More on Precision
//update beyond the precision values -- too many digits overall
car2 = car3;
car2.setCost(new BigDecimal("123456.66"));
try {
em.flush();
fail("database accepted too many digits");
} catch (PersistenceException ex) {
log.info("caught expected exception:" + ex);
}
-caught expected exception:javax.persistence.PersistenceException: org.hibernate.exception.DataException: could not execute statement
Entity using PROPERTY access
Entity class contains property that complies with default rules that should *not* be mapped to database
setters
getters
relations to other classes, including collections
Figure 47.3. Transient Example Entity Class
@Entity
@Table(name="ORMCORE_TANK")
public class Tank {
private long id;
private String make;
private String model;
public Tank() {}
public Tank(long id) { this.id = id; }
@Id
public long getId() { return id; }
protected void setId(long id) {
this.id = id;
}
@Transient //if you remove this, it will fail trying to locate setter
public String getMakeModel() {
return make + " " + model;
}
public String getMake() { return make; }
public void setMake(String make) {
this.make = make;
}
public String getModel() { return model; }
public void setModel(String model) {
this.model = model;
}
Figure 47.4. Transient Example orm.xml
<entity class="ejava.examples.orm.core.mapped.Tank" access="PROPERTY">
<table name="ORMCORE_TANK"/>
<attributes>
<id name="id"/>
<transient name="makeModel"/>
</attributes>
</entity>
Figure 47.5. Transient Example Database Schema
create table ORMCORE_TANK ( id bigint not null, make varchar(255), model varchar(255), primary key (id) )
Figure 47.6. Transient Example Test
ejava.examples.orm.core.annotated.Tank tank = new Tank(1);
tank.setMake("acme");
tank.setModel("great guns");
//insert a row in the database
em.persist(tank);
log.info("created tank:" + tank);
-created tank:ejava.examples.orm.core.annotated.Tank@4eef4eb7make=acme, model=great guns
Storing large text document as CLOB
Storing large binary data as BLOB
Figure 47.7. CLOB Example Database Schema
create table ORMCORE_UMBRELLA ( id bigint not null, make clob, model varchar(255), primary key (id) )
Figure 47.8. CLOB Example Entity Class
@Entity
@Table(name="ORMCORE_UMBRELLA")
public class Umbrella {
@Lob
@Basic(fetch=FetchType.LAZY) //ignored
public char[] getMake() {
return make.toCharArray();
}
public void setMake(char[] make) {
this.make = new String(make);
}
Figure 47.9. Temporal/Enum Example Schema
create table ORMCORE_VASE (
id bigint not null,
aDate date,
aTime time,
aTimestamp timestamp,
colorId integer,
colorName varchar(255),
primary key (id)
)
Figure 47.10. Temporal/Enum Example Entity Class
@Entity
@Table(name="ORMCORE_VASE")
public class Vase {
@Id
private long id;
@Temporal(TemporalType.DATE)
private Date aDate;
@Temporal(TemporalType.TIME)
private Date aTime;
@Temporal(TemporalType.TIMESTAMP)
private Date aTimestamp;
@Enumerated(EnumType.ORDINAL)
private ColorType colorId;
@Enumerated(EnumType.STRING)
private ColorType colorName;
Figure 47.11. Temporal/Enum Example orm.xml
<entity class="ejava.examples.orm.core.mapped.Vase" access="FIELD">
<table name="ORMCORE_VASE"/>
<attributes>
<id name="id"/>
<basic name="aDate">
<temporal>DATE</temporal>
</basic>
<basic name="aTime">
<temporal>TIME</temporal>
</basic>
<basic name="aTimestamp">
<temporal>TIMESTAMP</temporal>
</basic>
<basic name="colorId">
<enumerated>ORDINAL</enumerated>
</basic>
<basic name="colorName">
<enumerated>STRING</enumerated>
</basic>
</attributes>
</entity>
Figure 47.12. Temporal/Enum Example Test
@Test
public void testValues() {
log.info("testValues");
ejava.examples.orm.core.annotated.Vase vase = new Vase(1);
Date date = new Date();
vase.setADate(date);
vase.setATime(date);
vase.setATimestamp(date);
vase.setColorId(ColorType.RED);
vase.setColorName(ColorType.RED);
//insert a row in the database
em.persist(vase);
log.info("created case:" + vase);
//find the inserted object
em.flush();
em.clear();
Vase vase2 = em.find(Vase.class, 1L);
log.info("found vase:" + vase2);
-created case:ejava.examples.orm.core.annotated.Vase@7f68f33c, id=1, aDate=Mon Sep 23 00:11:10 EDT 2013, aTime=Mon Sep 23 00:11:10 EDT 2013, aTimestamp=Mon Sep 23 00:11:10 EDT 2013, colorId=RED, colorName=RED -found vase:ejava.examples.orm.core.annotated.Vase@5f0d8b74, id=1, aDate=2013-09-23, aTime=00:11:10, aTimestamp=2013-09-23 00:11:10.296, colorId=RED, colorName=RED
select * from ORMCORE_VASE ID ADATE ATIME ATIMESTAMP COLORID COLORNAME -- ---------- -------- ----------------------------- ------- --------- 1 2006-09-23 14:08:22 2006-09-23 14:08:22.221000000 0 RED
Notice impact of temporal DB-mapping does effect until data saved and retrieved from database
Every entity must have a primary key
Primary keys must be unique
Map to one ("simple") or more ("composite") properties
Properties must be of type
Java primitive types -- including object wrappers
java.lang.String
Custom classes made up of legal property types
Persistence providers required to provide primary key generation
Specific type of generator specified through a strategy
Figure 48.1. Specifying Primary Key Generation thru Annotations
@Entity
@Table(name="ORMCORE_DRILL")
public class Drill {
@Id
@GeneratedValue( //AUTO is the default and could be left off here
strategy=GenerationType.AUTO)
private long id; //unassigned PK value must be zero
private String make;
...
Figure 48.2. Specifying Primary Key Generation thru orm.xml
<entity class="ejava.examples.orm.core.mapped.Drill" access="FIELD">
<table name="ORMCORE_DRILL"/>
<attributes>
<id name="id">
<generated-value strategy="AUTO"/>
</id>
</attributes>
</entity>
Three (+1) Types
SEQUENCE
Database generates unique value from a global sequence
IDENTITY
Database generates unique value on a per-table basis
TABLE
AUTO
Provider may choose any technique, including one not specified above
All providers required to provide a default identity strategy
Can be specifically referenced using "strategy=GenerationType.AUTO"
Figure 48.3. GeneratedValue.strategy default
package javax.persistence;
...
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface GeneratedValue {
/**
* (Optional) The primary key generation strategy
* that the persistence provider must use to
* generate the annotated entity primary key.
*/
GenerationType strategy() default AUTO;
Figure 48.4. Entity with Default GenerationType (AUTO)
@Entity
@Table(name="ORMCORE_DRILL")
public class Drill {
@Id
@GeneratedValue
private long id;
private String make;
...
Figure 48.5. AUTO (Success) Test
Test
logger.info("testAUTOGood");
//since PKs are generated, we must pass in an object that
//has not yet been assigned a PK value.
ejava.examples.orm.core.annotated.Drill drill = new Drill(0);
drill.setMake("acme");
//insert a row in the database
logger.info("just before persist(tx={}): {}", txActive(), drill);
em.persist(drill);
logger.info("created drill (after persist and before flush, tx={}): {}", txActive(), drill);
em.flush();
logger.info("created drill (after flush, tx={}): {}", txActive(), drill);
assertNotEquals(0, drill.getId());
Output
-testAUTOGood -just before persist(tx=true): 1171672359, id=0, make=acme -call next value for hibernate_sequence -created drill (after persist and before flush, tx=true): 1171672359, id=1, make=acme -insert into ORMCORE_DRILL (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [acme] -binding parameter [2] as [BIGINT] - [1] -created drill (after flush, tx=true): 1171672359, id=1, make=acme
AUTO in this case meant SEQUENCE -- more details in sequence section
Figure 48.6. AUTO (Failure) Test
Test
logger.info("testAUTOBad");
//provider will not like the non-zero PK value here
//because we told it to generate the PK
ejava.examples.orm.core.annotated.Drill drill = new Drill(25L);
drill.setMake("BD");
//insert a row in the database
boolean exceptionThrown = false;
try {
assertNotEquals(0, drill.getId());
logger.info("trying to create drill with pre-exist pk: {}", drill);
em.persist(drill);
}
catch (PersistenceException ex) {
logger.info("got expected exception: " + ex);
exceptionThrown = true;
}
assertTrue(exceptionThrown);
Output
-testAUTOBad -trying to create drill with pre-exist pk: 1171672359, id=25, make=BD -got expected exception: javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: ejava.examples.orm.core.annotated.Drill
If an entity is defined to have its primary key automatically generated -- no matter the strategy -- the provider will insist on implementing the value. It is always an error to pass a detached/transient object to persist() with an id already assigned.
One should always identify what the GenerationStrategy should be so that the strategy remains consistent over time. For example, Hibernate use to default to a form of IDENTITY in JavaSE environments and SEQUENCE in JavaEE environments.
Uses database identity column type
If value provided to database -- it is used
If value not provided to database -- a follow-up call required to obtain value used
Not supported by all databases (e.g., Oracle)
Figure 48.7. Entity with GenerationType.IDENTITY
@Entity
@Table(name="ORMCORE_GADGET")
public class Gadget {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private long id;
private String make;
Figure 48.8. IDENTITY Database Schema
create table ORMCORE_GADGET (
id bigint generated by default as identity,
make varchar(255),
primary key (id)
)
Figure 48.9. IDENTITY Test (with Active Transaction)
Test (with Transaction Active)
ejava.examples.orm.core.annotated.Gadget gadget = new Gadget(0);
gadget.setMake("gizmo 1");
//insert a row in the database
//start with a tx already active
logger.info("gadget (before persist; tx={}): {}", txActive(), gadget);
em.persist(gadget);
logger.info("created gadget (after persist, before flush; tx={}): {}", txActive(), gadget);
em.flush();
logger.info("created gadget (after flush; tx={}): {}", txActive(), gadget);
assertNotEquals(0, gadget.getId());
Output (with Transaction Active)
-gadget (before persist; tx=true): 1798443618, id=0, make=gizmo 1 -insert into ORMCORE_GADGET (id, make) values (null, ?) -binding parameter [1] as [VARCHAR] - [gizmo 1] -created gadget (after persist, before flush; tx=true): 1798443618, id=1, make=gizmo 1 -created gadget (after flush; tx=true): 1798443618, id=1, make=gizmo 1
Follow-on IDENTITY Allocations (with Transaction Active)
-insert into ORMCORE_GADGET (id, make) values (null, ?) -binding parameter [1] as [VARCHAR] - [gizmo 2] -created gadget(tx=true): 370055648, id=2, make=gizmo 2 -insert into ORMCORE_GADGET (id, make) values (null, ?) -binding parameter [1] as [VARCHAR] - [gizmo 3] -created gadget(tx=true): 911933063, id=3, make=gizmo 3 -insert into ORMCORE_GADGET (id, make) values (null, ?) -binding parameter [1] as [VARCHAR] - [gizmo 4] -created gadget(tx=true): 568613487, id=4, make=gizmo 4
Provider inserts row into database with null ID
Provider queries database (not shown in debug) for primary key generated for the row
Provider eagerly inserts row during persist() and before flush() when transaction is open -- to determine primary key
When using the IDENTITY strategy, the provider must make at least two calls to the database. One for the INSERT and one to determine the primary key value generated by the database.
Figure 48.10. IDENTITY Test (persist() with Inactive Transaction)
Test
em.getTransaction().rollback();
logger.info("rolled back tx(tx={})", txActive());
for (int i=0; i<3; i++) {
Gadget g = new Gadget();
g.setMake("gizmo " + counter++);
em.persist(g);
logger.info("created gadget(tx={}): {}", txActive(), g);
if (i==0) {
gadget=g;
}
}
logger.info("starting tx(tx={}): {}", txActive(), gadget);
em.getTransaction().begin();
logger.info("tx started, flushing (tx={}): {}", txActive(), gadget);
em.flush();
logger.info("cache flushed (tx={}): {}", txActive(), gadget);
em.getTransaction().commit();
logger.info("tx committed (tx={}): {}", txActive(), gadget);
Output (persist() with inactive transaction)
-rolled back tx(tx=false)
-created gadget(tx=false): 1372646511, id=0, make=gizmo 5
-created gadget(tx=false): 1202178366, id=0, make=gizmo 6
-created gadget(tx=false): 1872410525, id=0, make=gizmo 7
-starting tx(tx=false): 1372646511, id=0, make=gizmo 5
Provider could not insert rows or determine primary key while transaction inactive
Business logic could not rely on PK values being set (e.g., send event or log)
Output (active transaction following persist)
-tx started, flushing (tx=true): 1372646511, id=0, make=gizmo 5
-insert into ORMCORE_GADGET (id, make) values (null, ?)
-binding parameter [1] as [VARCHAR] - [gizmo 5]
-insert into ORMCORE_GADGET (id, make) values (null, ?)
-binding parameter [1] as [VARCHAR] - [gizmo 6]
-insert into ORMCORE_GADGET (id, make) values (null, ?)
-binding parameter [1] as [VARCHAR] - [gizmo 7]
-cache flushed (tx=true): 1372646511, id=5, make=gizmo 5
-tx committed (tx=false): 1372646511, id=5, make=gizmo 5
Provider waited for next flush() cycle (not tx start) to perform INSERTs and determine primary key
Uses a formal database SEQUENCE construct to generate a unique ID
SEQUENCE is not part of a transaction -- cannot rollback the generation of a sequence value
Database SEQUENCE value may have gaps
Common, but not supported by all databases (e.g., HSQL)
Figure 48.11. Entity with GenerationType.SEQUENCE
@Entity
@Table(name="ORMCORE_FAN")
@SequenceGenerator(
name="fanSequence", //required logical name
sequenceName="FAN_SEQ", //name in database
initialValue=5, //start with something odd to be noticeable
allocationSize=3) //number of IDs to internally assign per-sequence value
public class Fan {
@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE, //use DB sequence
generator="fanSequence") //point to logical def
private long id;
private String make;
...
Figure 48.12. SEQUENCE Database Schema
create table ORMCORE_FAN ( id bigint not null, make varchar(255), primary key (id) ) create sequence FAN_SEQ start with 5 increment by 3
Provider generates schema with SEQUENCE starting with 5 with increment of 3
Inspecting database server through UI prior to start -- showed current value of 2 (2+3=5)
Figure 48.13. SEQUENCE Test (with active transaction)
Test (with active transaction)
Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("sql.sequences", "true")));
ejava.examples.orm.core.annotated.Fan fan = new Fan(0);
fan.setMake("cool runner 1");
//insert a row in the database
logger.info("persisting fan(tx={}): {}", txActive(), fan);
em.persist(fan);
logger.info("created fan (before flush, tx={}):", txActive(), fan);
em.flush();
logger.info("created fan (after flush; tx={}): {}", txActive(), fan);
assertNotEquals(0, fan.getId());
Output (with active transaction)
-persisting fan(tx=true): 1413306467, id=0, make=cool runner 1 -call next value for FAN_SEQ -call next value for FAN_SEQ #Current value=8 after this call, next result will be 11 -created fan (before flush, tx=true): 1413306467, id=5, make=cool runner 1 -insert into ORMCORE_FAN (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [cool runner 1] -binding parameter [2] as [BIGINT] - [5] -created fan (after flush; tx=true): 1413306467, id=5, make=cool runner 1 -persisting fan(tx=true): 1413306467, id=0, make=cool runner 1 -call next value for FAN_SEQ -call next value for FAN_SEQ -created fan (before flush, tx=true): 1413306467, id=5, make=cool runner 1 -insert into ORMCORE_FAN (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [cool runner 1] -binding parameter [2] as [BIGINT] - [5] -created fan (after flush; tx=true): 1413306467, id=5, make=cool runner 1
Provider obtains primary key value prior to inserting row (x2 calls addressed later)
First primary key value (5) corresponds with the @SequenceGenerator.initialValue property
Primary key value available to business logic prior to insert into database
Provider inserts the row during next flush cycle
Inspecting database server through UI -- shows current value of 8 (2x calls to next value)
The database advances its SEQUENCE by the allocationSize on each call to next value. The provider may use the returned value and allacationSize values above the returned value before returning to the database for a new value. The database and provider *must* have the same increment/allocationSize configured.
Figure 48.14. Follow-on SEQUENCE Allocations
Output prior to flush/commit
-created fan(tx=true): 1289462509, id=6, make=cool runner 2 -created fan(tx=true): 740265405, id=7, make=cool runner 3 -created fan(tx=true): 1439003682, id=8, make=cool runner 4 -call next value for FAN_SEQ -created fan(tx=true): 578969118, id=9, make=cool runner 5 -created fan(tx=true): 493310435, id=10, make=cool runner 6 -created fan(tx=true): 757436159, id=11, make=cool runner 7 -call next value for FAN_SEQ -created fan(tx=true): 1682973478, id=12, make=cool runner 8 ... -created fan(tx=true): 1501844857, id=22, make=cool runner 18 -created fan(tx=true): 817994751, id=23, make=cool runner 19 -call next value for FAN_SEQ -created fan(tx=true): 1312250810, id=24, make=cool runner 20 -created fan(tx=true): 1296316112, id=25, make=cool runner 21 ...
Provider calling database to get next allocation prior to exhausting current allocation
Provider knows to self-generate next allocationSize values for value returned prior to obtaining next allocation
Provider waits for next flush cycle to insert rows into database
Output during commit
-committing (tx=true): 1289462509, id=6, make=cool runner 2 -insert into ORMCORE_FAN (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [cool runner 2] -binding parameter [2] as [BIGINT] - [6] -insert into ORMCORE_FAN (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [cool runner 3] -binding parameter [2] as [BIGINT] - [7] -insert into ORMCORE_FAN (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [cool runner 4] ... -insert into ORMCORE_FAN (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [cool runner 21] -binding parameter [2] as [BIGINT] - [25] -tx committed (tx=false): 1289462509, id=6, make=cool runner 2
Rows inserted into database during next flush cycle triggered by commit
Database makes no correlation between sequence numbers returned and primary key value assigned to a row
The default increment/allocationSize requires two (2) calls per insert. A larger increment/allocationSize can reduce the number of calls by up to 50%, but will potentially leave gaps and exhaust the unique sequence values earlier in the lifetime of the database if clients terminate and restart prior to exhausting an allocation.
Figure 48.15. SEQUENCE Test (persist without active transaction)
Test
logger.info("tx(tx={})", txActive());
for (int i=0; i<20; i++) {
Fan f = new Fan();
f.setMake("cool runner " + counter++);
em.persist(f);
logger.info("created fan(tx={}): {}", txActive(), f);
if (i==0) {
fan=f;
}
}
logger.info("starting tx(tx={}): {}", txActive(), fan);
em.getTransaction().begin();
logger.info("tx started, flushing (tx={}): {}", txActive(), fan);
em.flush();
logger.info("cache flushed (tx={}): {}", txActive(), fan);
em.getTransaction().commit();
logger.info("tx committed (tx={}): {}", txActive(), fan);
Output while transaction inactive
-tx(tx=false) -created fan(tx=false): 1451387509, id=26, make=cool runner 22 -call next value for FAN_SEQ -created fan(tx=false): 1238209644, id=27, make=cool runner 23 -created fan(tx=false): 1371953731, id=28, make=cool runner 24 -created fan(tx=false): 1947060963, id=29, make=cool runner 25 -call next value for FAN_SEQ -created fan(tx=false): 1309934743, id=30, make=cool runner 26 -created fan(tx=false): 833420622, id=31, make=cool runner 27 -created fan(tx=false): 1601333072, id=32, make=cool runner 28 ... -call next value for FAN_SEQ -created fan(tx=false): 1591063329, id=42, make=cool runner 38 -created fan(tx=false): 2129344690, id=43, make=cool runner 39 -created fan(tx=false): 223662325, id=44, make=cool runner 40 -call next value for FAN_SEQ -created fan(tx=false): 1835794313, id=45, make=cool runner 41
Same as transaction active so far
Output when transaction activated
-starting tx(tx=false): 1451387509, id=26, make=cool runner 22 -tx started, flushing (tx=true): 1451387509, id=26, make=cool runner 22 -insert into ORMCORE_FAN (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [cool runner 22] -binding parameter [2] as [BIGINT] - [26] -insert into ORMCORE_FAN (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [cool runner 23] -binding parameter [2] as [BIGINT] - [27] ... -insert into ORMCORE_FAN (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [cool runner 41] -binding parameter [2] as [BIGINT] - [45] -cache flushed (tx=true): 1451387509, id=26, make=cool runner 22 -tx committed (tx=false): 1451387509, id=26, make=cool runner 22
As with previous case -- nothing started with transaction opening
All inserts are delayed until next flush cycle
A poor man's SEQUENCE
A generic answer to "some databases don't support ..."
Both can allocate IDs in blocks and allow the client provider to assign IDs with less requests to the database
SEQUENCES are implemented in the database as non-transactional resources for this specific purpose
The TABLEs strategy is implemented using RDBMS Table and is transactional
The deterministic read and update of a RDBMS table requires a transaction with a at least a row lock that will allow a read, increment, and update a column value for a row
Transactions and locks required on the ID table row -- may induce sequential access to inserts
Figure 48.16. Entity with GenerationType.TABLE
@Entity
@Table(name="ORMCORE_EGGBEATER")
@TableGenerator( //note that all but name are optional if generating schema
name="eggbeaterGenerator", //logical name of generator
table="ORMCORE_EB_UID", //name of table storing seq
pkColumnName="UID_ID", //pk column for seq table
pkColumnValue="ORMCORE_EGGBEATER", //pk value in pk column
valueColumnName="UID_VAL", //column for seq value
initialValue=7, //first value database should provide
allocationSize=5 //increment UID_ID after using this many
)
public class EggBeater {
@Id
@GeneratedValue(strategy=GenerationType.TABLE, //use DB table
generator="eggbeaterGenerator") //point to logical def
private long id;
private String make;
...
Figure 48.17. TABLE Database Schema
create table ORMCORE_EGGBEATER ( id bigint not null, make varchar(255), primary key (id) ) create table ORMCORE_EB_UID ( UID_ID varchar(255), UID_VAL integer ) -insert into ORMCORE_EB_UID(UID_ID, UID_VAL) values ('ORMCORE_EGGBEATER',7) -insert into ORMCORE_EB_UID(UID_ID, UID_VAL) values ('ORMCORE_EGGBEATER',7)
Row insert initializes ID row
Duplicate row inserts likely caused by demonstration environment having annotated and mapped classes define same table mapping
Figure 48.18. TABLE Test
logger.info("testTABLE");
logger.debug("table id before(tx={})={}", txActive(), getTableId());
//note that since PKs are generated, we must pass in an object that
//has not yet been assigned a PK value.
ejava.examples.orm.core.annotated.EggBeater eggbeater = new EggBeater(0);
eggbeater.setMake("done right 1");
//insert a row in the database
logger.info("persisting eggbeater (tx={}): {}", txActive(), eggbeater);
em.persist(eggbeater);
logger.info("created eggbeater (before flush; tx={}): {}", txActive(), eggbeater);
em.flush();
logger.info("created eggbeater (after flush; tx={}): {}", txActive(), eggbeater);
assertNotEquals(0, eggbeater.getId());
logger.debug("table id after(tx={})={}", txActive(), getTableId());
-testTABLE -select UID_VAL from ORMCORE_EB_UID where UID_ID='ORMCORE_EGGBEATER' -extracted value ([UID_VAL] : [NUMERIC]) - [7] -table id before(tx=true)=7 -persisting eggbeater (tx=true): 1049628186, id=0, make=done right 1 -select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update -update ORMCORE_EB_UID set UID_VAL=? where UID_VAL=? and UID_ID=? -select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update -update ORMCORE_EB_UID set UID_VAL=? where UID_VAL=? and UID_ID=? -created eggbeater (before flush; tx=true): 1049628186, id=8, make=done right 1 -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 1] -binding parameter [2] as [BIGINT] - [8] -created eggbeater (after flush; tx=true): 1049628186, id=8, make=done right 1 -select UID_VAL from ORMCORE_EB_UID where UID_ID='ORMCORE_EGGBEATER' -extracted value ([UID_VAL] : [NUMERIC]) - [17] -table id after(tx=true)=17
Provider gets a primary key value during persist
Provider locks row (SELECT FOR UPDATE) during transaction and updates with new value
Provider inserts row during flush cycle with generated primary key value
Cannot explain the initial 2x ID requests but both incremented the value to a result of 17 (7+5+5=17)
Figure 48.19. Follow-on TABLE Allocations
-created ehhbeater(tx=true): 1275580924, id=9, make=done right 2 -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 2] -binding parameter [2] as [BIGINT] - [9] -table id after[2](tx=true)=17 -created ehhbeater(tx=true): 1726759945, id=10, make=done right 3 -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 3] -binding parameter [2] as [BIGINT] - [10] -table id after[3](tx=true)=17 ... -created ehhbeater(tx=true): 154468798, id=13, make=done right 6 -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 6] -binding parameter [2] as [BIGINT] - [13] -table id after[6](tx=true)=17 -select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update -update ORMCORE_EB_UID set UID_VAL=? where UID_VAL=? and UID_ID=? -created ehhbeater(tx=true): 490475818, id=14, make=done right 7 -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 7] -binding parameter [2] as [BIGINT] - [14] -table id after[7](tx=true)=22 ... -created ehhbeater(tx=true): 360233196, id=18, make=done right 11 -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 11] -binding parameter [2] as [BIGINT] - [18] -table id after[11](tx=true)=22 -select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update -update ORMCORE_EB_UID set UID_VAL=? where UID_VAL=? and UID_ID=? -created ehhbeater(tx=true): 1912769093, id=19, make=done right 12 -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 12] -binding parameter [2] as [BIGINT] - [19] -table id after[12](tx=true)=27 ... -created ehhbeater(tx=true): 1947681232, id=23, make=done right 16 -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 16] -binding parameter [2] as [BIGINT] - [23] -table id after[16](tx=true)=27 -select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update -update ORMCORE_EB_UID set UID_VAL=? where UID_VAL=? and UID_ID=? -created ehhbeater(tx=true): 783682673, id=24, make=done right 17 -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 17] -binding parameter [2] as [BIGINT] - [24] -table id after[17](tx=true)=32 ... -committing (tx=true): 1275580924, id=9, make=done right 2 -tx committed (tx=false): 1275580924, id=9, make=done right 2 ...
As will SEQUENCE, the TABLE strategy allows each client to generate an allocationSize amount of primary key values before requiring a flush of the current batch or polling for a new table value.
Figure 48.20. TABLE Test (persist without active transaction)
Output prior to transaction
-tx(tx=false) -created ehhbeater(tx=false): 150835665, id=29, make=done right 22 -table id after[22](tx=false)=37 -created ehhbeater(tx=false): 315885065, id=30, make=done right 23 -table id after[23](tx=false)=37 -created ehhbeater(tx=false): 423539130, id=31, make=done right 24 -table id after[24](tx=false)=37 -created ehhbeater(tx=false): 841313896, id=32, make=done right 25 -table id after[25](tx=false)=37 -created ehhbeater(tx=false): 1673518027, id=33, make=done right 26 -table id after[26](tx=false)=37 -select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update -update ORMCORE_EB_UID set UID_VAL=? where UID_VAL=? and UID_ID=? -created ehhbeater(tx=false): 1042223174, id=34, make=done right 27 -table id after[27](tx=false)=42 ... -created ehhbeater(tx=false): 584643821, id=38, make=done right 31 -table id after[31](tx=false)=42 -select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update -update ORMCORE_EB_UID set UID_VAL=? where UID_VAL=? and UID_ID=? -created ehhbeater(tx=false): 999999316, id=39, make=done right 32 -table id after[32](tx=false)=47 ... -created ehhbeater(tx=false): 1815337594, id=43, make=done right 36 -table id after[36](tx=false)=47 -select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update -update ORMCORE_EB_UID set UID_VAL=? where UID_VAL=? and UID_ID=? -created ehhbeater(tx=false): 362311125, id=44, make=done right 37 -table id after[37](tx=false)=52 ... -created ehhbeater(tx=false): 1292683326, id=48, make=done right 41 -table id after[41](tx=false)=52
Provider obtains next allocation value outside of transaction where rows inserted
Next allocation value requires transaction to update -- results in extra transaction per allocation
Provider assigns ID to object from allocation prior to transaction where rows are inserted
Output once transaction started
-starting tx(tx=false): 150835665, id=29, make=done right 22 -tx started, flushing (tx=true): 150835665, id=29, make=done right 22 -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 22] -binding parameter [2] as [BIGINT] - [29] -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 23] -binding parameter [2] as [BIGINT] - [30] -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 24] -binding parameter [2] as [BIGINT] - [31] -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 25] -binding parameter [2] as [BIGINT] - [32] -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 26] -binding parameter [2] as [BIGINT] - [33] ... -insert into ORMCORE_EGGBEATER (make, id) values (?, ?) -binding parameter [1] as [VARCHAR] - [done right 41] -binding parameter [2] as [BIGINT] - [48] -cache flushed (tx=true): 150835665, id=29, make=done right 22 -tx committed (tx=false): 150835665, id=29, make=done right 22
Provider inserts rows from cache during next flush cycle
Each row has a pre-assigned ID from provider
Database makes no correlation between ID within row and the table maintaining the IDs
Primary key consisting of multiple properties
Represented using a primary key class
Must implement Serializable
public class MowerPK implements Serializable {
Must have a public no-arg constructor
public MowerPK() {}
Must implement hashCode() and equals() methods
@Override
public int hashCode() { ... }
@Override
public boolean equals(Object obj) { ... }
Must define properties that match with the definition of the entity, including access
private String make; private String model;
Accessed using same strategy as entity using it
package ejava.examples.orm.core;
import java.io.Serializable;
public class MowerPK implements Serializable {
private static final long serialVersionUID = 1L;
private String make;
private String model;
public MowerPK() {}
public MowerPK(String make, String model) {
this.make = make;
this.model = model;
}
public String getMake() { return make; }
public String getModel() { return model; }
@Override
public int hashCode() { return make.hashCode() + model.hashCode(); }
@Override
public boolean equals(Object obj) {
try {
if (this == obj) return true;
return make.equals(((MowerPK)obj).getMake()) &&
model.equals(((MowerPK)obj).getModel());
} catch (Throwable ignored) { //catch NP & Cast Exceptions
return false;
}
}
...
}
A class separate from the entity that independently models PK properties
No reference to the @IdClass within the entity
Figure 49.1. Composite @IdClass Database Schema
create table ORMCORE_MOWER ( make varchar(255) not null, model varchar(255) not null, size integer not null, primary key (make, model) )
Figure 49.2. Composite @IdClass Example Usage
@Entity
@Table(name="ORMCORE_MOWER")
@IdClass(MowerPK.class)
public class Mower {
@Id
private String make;
@Id
private String model;
private int size;
public Mower() {}
public Mower(String make, String model) {
this.make = make;
this.model = model;
}
public String getMake() { return make; }
public String getModel() { return model; }
Figure 49.3. Composite @IdClass Example orm.xml
<entity class="ejava.examples.orm.core.mapped.Mower" access="FIELD">
<table name="ORMCORE_MOWER"/>
<id-class class="ejava.examples.orm.core.MowerPK"/>
<attributes>
<id name="make"/>
<id name="model"/>
</attributes>
</entity>
Figure 49.4. Composite @IdClass Example Client
Create Entity with Composite ID
ejava.examples.orm.core.annotated.Mower mower = new Mower("acme", "power devil2");
mower.setSize(21);
//insert a row in the database
logger.info("persisting mower(tx={}): {}", txActive(), mower);
em.persist(mower);
logger.info("created mower: {}", mower);
em.flush();
logger.info("flushed");
-persisting mower(tx=true): 1756435781, make=acme, model=power devil2, size=21
-created mower: 1756435781, make=acme, model=power devil2, size=21
-insert into ORMCORE_MOWER (size, make, model) values (?, ?, ?)
-binding parameter [1] as [INTEGER] - [21]
-binding parameter [2] as [VARCHAR] - [acme]
-binding parameter [3] as [VARCHAR] - [power devil2]
-flushed
Get Instance by Composite ID
//locate instance by ID, while instance still managed
Mower mower2 = em.find(Mower.class, new MowerPK("acme", "power devil2"));
assertNotNull(mower2);
logger.info("found mower: {}", mower2);
assertEquals(mower.getSize(), mower2.getSize());
-found mower: 1756435781, make=acme, model=power devil2, size=21
Instance is found in cache by ID without having to query database
Remove Instance from Database
em.remove(mower2);
logger.info("removed mower: {}", mower2);
em.flush();
logger.info("removed mower after flush: {}", mower2);
Mower mower3 = em.find(Mower.class, new MowerPK("acme", "power devil2"));
assertNull(mower3);
-removed mower: 1756435781, make=acme, model=power devil2, size=21
-delete from ORMCORE_MOWER
where make=? and model=?
-binding parameter [1] as [VARCHAR] - [acme]
-binding parameter [2] as [VARCHAR] - [power devil2]
-removed mower after flush: 1756435781, make=acme, model=power devil2, size=21
-select mower0_.make as make1_7_0_, mower0_.model as model2_7_0_, mower0_.size as size3_7_0_
from ORMCORE_MOWER mower0_
where mower0_.make=? and mower0_.model=?
-binding parameter [1] as [VARCHAR] - [acme]
-binding parameter [2] as [VARCHAR] - [power devil2]
Row removed from database and instance detached
Primary key used to query database since instance no longer in cache
A class that hosts the primary key properties
Contained/"embedded" within the entity class
Figure 49.5. @EmbeddedId Example Database Schema
create table ORMCORE_NAPSACK (
NAPSACK_MAKE varchar(255) not null,
NAPSACK_MODEL varchar(255) not null,
size integer not null,
primary key (NAPSACK_MAKE, NAPSACK_MODEL)
)
Figure 49.6. @Embeddable Primary Key Class
package ejava.examples.orm.core.mapped;
import java.io.Serializable;
import javax.persistence.*;
@Embeddable
public class NapsackPK implements Serializable {
private static final long serialVersionUID = 1L;
@Column(name="NAPSACK_MAKE") //maps field to column of containing class
private String make;
@Column(name="NAPSACK_MODEL")//maps field to column of containing class
private String model;
public NapsackPK() {}
public NapsackPK(String make, String model) {
this.make = make;
this.model = model;
}
public String getMake() { return make; }
public String getModel() { return model; }
public int hashCode() { return make.hashCode() + model.hashCode(); }
public boolean equals(Object obj) {
try {
if (this == obj) return true;
return make.equals(((NapsackPK)obj).getMake()) &&
model.equals(((NapsackPK)obj).getModel());
} catch (Throwable ignored) { //catch NP & Cast Exceptions
return false;
}
}
Figure 49.7. Composite @EmbeddedId Example Usage
@Entity
@Table(name="ORMCORE_NAPSACK")
public class Napsack {
@EmbeddedId
private NapsackPK pk;
private int size;
public Napsack() {}
public Napsack(String make, String model) {
this.pk = new NapsackPK(make, model);
}
public NapsackPK getPk() { return pk; }
...
Figure 49.8. Composite @EmbeddedId Example orm.xml
<entity class="ejava.examples.orm.core.mapped.Napsack" access="FIELD">
<table name="ORMCORE_NAPSACK"/>
<attributes>
<embedded-id name="pk"/>
</attributes>
</entity>
<embeddable class="ejava.examples.orm.core.mapped.NapsackPK">
<attributes>
<basic name="make">
<column name="NAPSACK_MAKE"/>
</basic>
<basic name="model">
<column name="NAPSACK_MODEL"/>
</basic>
</attributes>
</embeddable>
Supply or override mapping of primary key class by entity class
Figure 49.9. Example Overridden @Embeddable PK Class
@Embeddable
public class MakeModelPK implements Serializable {
private String make;
private String model;
Figure 49.10. Example Overridden @Embeddable Entity Schema
create table ORMCORE_PEN ( PEN_MAKE varchar(255) not null, PEN_MODEL varchar(255) not null, size integer not null, primary key (PEN_MAKE, PEN_MODEL) )
Figure 49.11. Example Overridden @Embeddable Entity Class
@Entity
@Table(name="ORMCORE_PEN")
public class Pen {
@EmbeddedId
@AttributeOverrides({
@AttributeOverride(name="make", column=@Column(name="PEN_MAKE")),
@AttributeOverride(name="model", column=@Column(name="PEN_MODEL"))
})
private MakeModelPK pk;
private int size;
public Pen() {}
public Pen(String make, String model) {
this.pk = new MakeModelPK(make, model);
}
public MakeModelPK getPk() { return pk; }
Figure 49.12. Example Overridden @Embeddableorm.xml
<entity class="ejava.examples.orm.core.mapped.Pen" access="FIELD">
<table name="ORMCORE_PEN"/>
<attributes>
<embedded-id name="pk">
<attribute-override name="make">
<column name="PEN_MAKE"/>
</attribute-override>
<attribute-override name="model">
<column name="PEN_MODEL"/>
</attribute-override>
</embedded-id>
</attributes>
</entity>
<embeddable class="ejava.examples.orm.core.mapped.MakeModelPK">
<attributes>
<basic name="make"/>
<basic name="model"/>
</attributes>
</embeddable>
PKClass Requirements
Serializable
No-arg constructor
hashCode() and equals()
PK properties
@IdClass
Independent class containing copy of entity PK properties
@Embeddable
Class instance containing entity PK properties -- embedded within entity
DB Mapping
Primary in PKClass
Overrides in Entity class
@AttributeOverrides
Map a single Java class to multiple tables in database
Uses a join of two or more tables and a single Java class
One table is designated as primary
Secondary and primary must have a column to join
Three tables (one primary and two secondary) joined
Figure 50.1. Multi-table Mapping Example Database Schema
create table ORMCORE_WATCH ( id bigint not null, make varchar(255), model varchar(255), primary key (id) )
create table ORMCORE_OWNER ( cardnum varchar(255), NAME varchar(255), OWNER_ID bigint not null, primary key (OWNER_ID) )
create table ORMCORE_MAKER ( address varchar(255), NAME varchar(255), phone varchar(255), MAKER_ID bigint not null, primary key (MAKER_ID) )
alter table ORMCORE_MAKER add constraint FKB91D15CEC0CE58E2 foreign key (MAKER_ID) references ORMCORE_WATCH alter table ORMCORE_OWNER add constraint FKB943503D985CE28 foreign key (OWNER_ID) references ORMCORE_WATCH
Figure 50.2. Multi-table Mapping Example Entity Class
@Entity
@Table(name="ORMCORE_WATCH")
@SecondaryTables({
@SecondaryTable(name="ORMCORE_OWNER",
pkJoinColumns={
@PrimaryKeyJoinColumn(name="OWNER_ID")}),
@SecondaryTable(name="ORMCORE_MAKER",
pkJoinColumns={
@PrimaryKeyJoinColumn(name="MAKER_ID")})
})
public class Watch {
@Id
private long id;
private String make;
private String model;
@Column(name="NAME", table="ORMCORE_OWNER")
private String owner;
@Column(table="ORMCORE_OWNER")
private String cardnum;
@Column(name="NAME", table="ORMCORE_MAKER")
private String manufacturer;
@Column(table="ORMCORE_MAKER")
private String address;
@Column(table="ORMCORE_MAKER")
private String phone;
Figure 50.3. Multi-table Mapping Example orm.xml
<entity class="ejava.examples.orm.core.mapped.Watch" access="FIELD">
<table name="ORMCORE_WATCH"/>
<secondary-table name="ORMCORE_OWNER">
<primary-key-join-column name="OWNER_ID"/>
</secondary-table>
<secondary-table name="ORMCORE_MAKER">
<primary-key-join-column name="MAKER_ID"/>
</secondary-table>
<attributes>
<id name="id"/>
<basic name="owner">
<column name="NAME" table="ORMCORE_OWNER"/>
</basic>
<basic name="cardnum">
<column table="ORMCORE_OWNER"/>
</basic>
<basic name="manufacturer">
<column name="NAME" table="ORMCORE_MAKER"/>
</basic>
<basic name="address">
<column table="ORMCORE_MAKER"/>
</basic>
<basic name="phone">
<column table="ORMCORE_MAKER"/>
</basic>
</attributes>
</entity>
Figure 50.4. Multi-table Mapping Example Test
//leave a watch in DB to inspect
Watch watch3 = new Watch(3);
watch3.setMake("ontime3");
watch3.setModel("round-and-round3");
watch3.setOwner("john doe3");
watch3.setCardnum("123-45-67893");
watch3.setManufacturer("getter done3");
watch3.setAddress("12noon lane3");
watch3.setPhone("410-555-12123");
em.persist(watch3);
select * from ORMCORE_WATCH ID MAKE MODEL 3 ontime3 round-and-round3
select * from ORMCORE_MAKER NAME PHONE ADDRESS MAKER_ID getter done3 410-555-12123 12noon lane3 3
select * from ORMCORE_OWNER CARDNUM NAME OWNER_ID 123-45-67893 john doe3 3
Similar to @EmbeddedId except embedded object is not a primary key
Embedded object has no primary key -- housed in parent entity table
Embedded object represents some abstraction
Parent entity represents an abstraction with a primary key and the embedded object
The primary key could be the only thing the parent entity is providing
Figure 51.1. Embedded Object Example Database Schema
create table ORMCORE_XRAY ( id bigint not null, address varchar(255), XRAY_MAKER varchar(255), phone varchar(255), model varchar(255), primary key (id) )
Figure 51.2. Embedded Object Example Embedded Class
@Embeddable
public class Manufacturer {
private String name;
private String address;
private String phone;
Figure 51.3. Embedded Object Example Entity Class
@Entity
@Table(name="ORMCORE_XRAY")
public class XRay {
@Id
private long id;
@Embedded
@AttributeOverrides({
@AttributeOverride(name="name", column=@Column(name="XRAY_MAKER"))
//note that we are letting address and phone default
})
private Manufacturer maker;
private String model;
Figure 51.4. Embedded Object Example orm.xml
<entity class="ejava.examples.orm.core.mapped.XRay" access="FIELD">
<table name="ORMCORE_XRAY"/>
<attributes>
<id name="id"/>
<embedded name="maker">
<attribute-override name="name">
<column name="XRAY_MAKER"/>
</attribute-override>
<!-- address and phone will default to a column name -->
</embedded>
<!-- model will default to a column name -->
</attributes>
</entity>
<embeddable class="ejava.examples.orm.core.mapped.Manufacturer" access="FIELD"/>
Figure 51.5. Embedded Object Example Test
XRay xray3 = new XRay(3);
xray3.setModel("inside-counts");
xray3.setMaker(
new Manufacturer("hi-tech", "low valley", "410-555-1212"));
em.persist(xray3);
ID PHONE ADDRESS XRAY_MAKER MODEL -- ------------ ---------- ---------- ------------- 3 410-555-1212 low valley hi-tech inside-counts
Table of Contents
Explore capabilities of Validation API
Introduce the JPA persistence lifecycle
Demonstrate Validation API integration with JPA lifecycle
Integrate validation into Maven builds
Independent of other APIs (e.g., persistence, web-tier)
Available server-side and client-side
Provide validation metadata thru annotations and XML descriptors
Applied to FIELDs, METHODs, TYPEs, etc.
import javax.validation.constraints.*;
public class Person {
private int id;
@NotNull
@Size(min=1,max=12)
@Pattern(regexp="^[a-zA-Z\\ \\-]+$", message="invalid characters in name")
private String firstName;
@NotNull
@Size(min=1,max=20)
@Pattern(regexp="^[a-zA-Z\\ \\-]+$", message="invalid characters in name")
private String lastName;
@Past
private Date birthDate;
@Size(min=7,max=50)
@Pattern(regexp="^.+@.+\\..+$")
private String email;
Obtain instance of Validator from ValidationFactory
private ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
private Validator val = vf.getValidator();
Validate bean instances
Person p = new Person()
.setFirstName("Bob2")
.setLastName("Smith")
.setEmail("bob2");
Set<ConstraintViolation<Person>> violations = val.validate(p);
for (ConstraintViolation<Person> v : violations) {
logger.info("{}:{} {}", v.getPropertyPath(), v.getInvalidValue(), v.getMessage());
}
assertEquals("unexpected number of violations", 3, violations.size());
-email:bob2 size must be between 7 and 50 -email:bob2 must match "^.+@.+\..+$" -firstName:Bob2 invalid first name
Logical grouping of constraints
Used to define which constraints and activated when
Can be used to define sequences -- with short-circuit
package ejava.jpa.example.validation;
import javax.validation.groups.Default;
public interface Drivers extends Default {}
package ejava.jpa.example.validation;
import javax.validation.groups.Default;
public interface POCs extends Default {}
public class Person {
...
@NotNull(groups={Drivers.class, POCs.class})
@Past(groups=Drivers.class)
private Date birthDate;
@NotNull(groups=POCs.class)
@Size(min=7,max=50)
@Pattern(regexp="^.+@.+\\..+$")
private String email;
Validate named groups of constraints
Person p = new Person()
.setFirstName("Bob")
.setLastName("Smith")
.setEmail("bob.smith@gmail.com")
.setBirthDate(new Date(System.currentTimeMillis()+100000));
Set<ConstraintViolation<Person>> validPerson = val.validate(p, Default.class);
Set<ConstraintViolation<Person>> validDriver = val.validate(p, Drivers.class);
Set<ConstraintViolation<Person>> validPOC = val.validate(p, POCs.class);
assertTrue("not validPerson", validPerson.isEmpty());
assertFalse("validDriver", validDriver.isEmpty());
assertTrue("not validPOC", validPOC.isEmpty());
Drivers group fails because birthDate in future
POC does not yet have a birthDate constraint
package ejava.jpa.example.validation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
import javax.validation.Constraint;
import javax.validation.Payload;
/**
* Defines a constraint annotation for expressing a minimum age.
*/
@Documented
@Constraint(validatedBy={MinAgeValidator.class})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
public @interface MinAge {
String message() default "too young";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default{};
int age() default 0;
}
@Constraint defines validator class
@Target defines what can be assigned to
FIELD - attributes
METHOD - getters
TYPE - classes
ANNOTATION_TYPE - constraints composing other constraints
PARAMETER - no support required by spec
CONSTRUCTOR - no support required by spec
Several reserved properties
message() - used to create error message
groups() - defines which groups constraint member of. Defaults to Default group
payload() - defines association for constraint
@MinAge(age=16, payload=Severity.Critical.class) private Date birthDate;
@NotNull(payload=Severity.Warning.class) private Date voterRegistrationDate;
names starting with "valid" - reserved/not allowed
Define use-specific properties (i.e., age)
Optionally define annotation for multiple annotations
public @interface MinAge {
...
/**
* Defines an array of annotations so that more than one can be applied.
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
public @interface List {
MinAge[] value();
}
}
@Documented adds this spec to API spec of what it annotates
package ejava.jpa.example.validation;
...
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class MinAgeValidator implements ConstraintValidator<MinAge, Date>{
int minAge;
@Override
public void initialize(MinAge constraint) {
this.minAge = constraint.age();
}
@Override
public boolean isValid(Date date, ConstraintValidatorContext ctx) {
if (date==null) { return true; }
//get today's date
Calendar latestBirthDate = new GregorianCalendar();
latestBirthDate.add(Calendar.YEAR, -1*minAge);
//get calendate date of object
Calendar birthDate = new GregorianCalendar();
birthDate.setTime(date);
if (birthDate.after(latestBirthDate)) {
String errorMsg = String.format("%d is younger than minimum %d",
getAge(birthDate),
minAge);
ctx.buildConstraintViolationWithTemplate(errorMsg)
.addConstraintViolation();
return false;
} else {
return true;
}
}
private int getAge(Calendar birth) {
...
...
}
public class Person {
...
@NotNull(groups={Drivers.class, POCs.class})
@MinAge.List({
@MinAge(age=18, groups=POCs.class),
@MinAge(age=16, groups=Drivers.class)
})
private Date birthDate;
...
Calendar fifteen = new GregorianCalendar();
fifteen.add(Calendar.YEAR, -16);
fifteen.add(Calendar.DAY_OF_YEAR, 2);
Person p = new Person()
.setFirstName("Bob")
.setLastName("Smith")
.setBirthDate(fifteen.getTime());
Set<ConstraintViolation<Person>> violations = val.validate(p, Drivers.class);
for (ConstraintViolation<Person> v : violations) {
logger.info("{}:{} {}", v.getPropertyPath(), v.getInvalidValue(), v.getMessage());
}
...
assertFalse("valid driver", violations.isEmpty());
Bob is too young to be a valid driver
-birthDate:Wed Jun 11 01:06:33 EDT 1997, 15 is younger than minimum 16 -birthDate:Wed Jun 11 01:06:33 EDT 1997, too young
public class Person {
...
@NotNull
@Size(min=1,max=12)
@Pattern(regexp="^[a-zA-Z\\ \\-]+$", message="invalid characters in name")
private String firstName;
@NotNull
@Size(min=1,max=20)
@Pattern(regexp="^[a-zA-Z\\ \\-]+$", message="invalid characters in name")
private String lastName;
Multiple constraints makeup complete definition
Verbose and tedious to define multiple times
public class Person {
...
@ValidName(min=1, max=12, regexp="^[a-zA-Z\\ \\-]+$", message="invalid first name")
private String firstName;
...
@ValidName(min=1, max=20, regexp="^[a-zA-Z\\ \\-]+$", message="invalid last name")
private String lastName;
...
/**
* Defines a validation composition
*/
@NotNull
@Size
@Pattern(regexp="")
@Documented
@Constraint(validatedBy={})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
public @interface ValidName {
String message() default "invalid name";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default{};
@OverridesAttribute(constraint=Size.class, name="min") int min() default 0;
@OverridesAttribute(constraint=Size.class, name="max") int max() default Integer.MAX_VALUE;
@OverridesAttribute(constraint=Pattern.class, name="regexp") String regexp() default ".*";
/**
* Defines an array of annotations so that more than one can be applied.
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
public @interface List {
ValidName[] value();
}
}
Annotate a composite constraint annotation with the building blocks
Define attribute overrides as appropriate
Person p = new Person()
.setFirstName("Bob")
.setLastName("Smithhhhhhhhhhhhhhhhhh$%$%$$$$$$$$$$$$$$$$");
Set<ConstraintViolation<Person>> violations = val.validate(p);
for (ConstraintViolation<Person> v : violations) {
logger.info("{}:{} {}", v.getPropertyPath(), v.getInvalidValue(), v.getMessage());
}
-lastName:Smithhhhhhhhhhhhhhhhhh$%$%$$$$$$$$$$$$$$$$ size must be between 1 and 20 -lastName:Smithhhhhhhhhhhhhhhhhh$%$%$$$$$$$$$$$$$$$$ must match "^[a-zA-Z\ \-]+$"
Bean failed two composed constraints
Define a sequence of validation groups to be tested in order and short-circuit upon failure
public interface DBChecks {} //checks whether data will fit within DB
public interface DataChecks {} //more detailed content checks
A set of Constraint Groups defined based on complexity/cost
@GroupSequence({Default.class, DBChecks.class, DataChecks.class})
public interface ValidationSequence {}
A Group Sequence defines the order
@Column(name="STREET", length=32, nullable=false)
@NotNull(message="street not supplied")
@Size(max=32, message="street name too large", groups=DBChecks.class)
@Pattern(regexp="^[0-9A-Za-z\\ ]+$", groups=DataChecks.class,
message="street must be numbers and letters")
private String street;
Constraints are assigned to Constraint Groups
We cause an error in the data size and content
Address a = new Address()
.setStreet("1600$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$")
.setCity("Washington")
.setState("DC")
.setZip("20500");
Set<ConstraintViolation<Address>> violations = val.validate(a, ValidationSequence.class);
...
//we should only get violations from the DB group
assertEquals("unexpected number of violations", 1, violations.size());
Notice Default passed, DBChecks failed, and DataChecks not attempted
-street:1600$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ street name too large
Look across properties of type
...
@Documented
@Constraint(validatedBy={CityStateOrZipValidator.class})
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
public @interface CityStateOrZip {
String message() default "must have city and state or zip code";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default{};
}
public class CityStateOrZipValidator implements ConstraintValidator<CityStateOrZip, Address1>{
@Override
public void initialize(CityStateOrZip constraintAnnotation) {}
@Override
public boolean isValid(Address1 address, ConstraintValidatorContext context) {
if (address==null) { return true; }
return (address.getCity()!=null && address.getState()!=null) ||
address.getZip()!=null;
}
}
Address1 a1 = new Address1()
.setStreet("1600")
.setCity("Washington");
Set<ConstraintViolation<Address1>> violations = val.validate(a1,PreCheck.class);
...
assertEquals("unexpected violation", 1, violations.size());
-:1600 Washington, null null, must have city and state or zip code
Traverse validation across relationships
public class Purchase {
@Valid
private Set<PurchaseItem> items;
Define Constraints external to Bean Class
Constraints need not be defined within bean class
public class Book {
private int id;
//@NotNull(message="title is required")
//@Size(max=32, message="title too long")
private String title;
//@Min(value=1, message="pages are required")
private int pages;
<validation-config xmlns="http://jboss.org/xml/ns/javax/validation/configuration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration">
<default-provider>org.hibernate.validator.HibernateValidator</default-provider>
<constraint-mapping>META-INF/book-constraints.xml</constraint-mapping>
</validation-config>
<constraint-mappings
xmlns="http://jboss.org/xml/ns/javax/validation/mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jbss.org/xml/ns/javax/validation/mapping
http://jboss.org/xml/ns/javax/validation/mapping/validation-mapping-1.0.xsd">
<bean class="ejava.jpa.example.validation.Book">
<field name="title">
<constraint annotation="javax.validation.constraints.NotNull">
<message>title is required</message>
</constraint>
<constraint annotation="javax.validation.constraints.Size">
<message>title is required</message>
<element name="max">32</element>
</constraint>
</field>
<field name="pages">
<constraint annotation="javax.validation.constraints.Min">
<message>pages are required</message>
<element name="value">1</element>
</constraint>
</field>
</bean>
</constraint-mappings>
Provide access to persistence lifecycle events
Persistence lifecycle events provided to class methods
@Entity
@Table(name="ORMLISTEN_PERSON")
public class Person {
@Id @GeneratedValue
private long id;
private String name;
@OneToOne(mappedBy="person", optional=true, cascade=CascadeType.ALL)
private Residence residence;
@PrePersist public void prePersist() {
}
@PostPersist public void postPersist() {
}
@PostLoad public void postLoad() {
}
@PreUpdate public void preUpdate() {
}
@PostUpdate public void postUpdate() {
}
@PreRemove public void preRemove() {
}
@PostRemove public void postRemove() {
}
}
Could be used to log or validate instance at certain stages of lifecycle
JPA prohibits calling EntityManager within callback methods
Persistence lifecycle events provided to external class
@Entity
@Table(name="ORMLISTEN_PERSON")
@EntityListeners(Listener.class)
public class Person {
public class Listener {
@PrePersist public void prePersist(Object entity) {
}
@PostPersist public void postPersist(Object entity) {
}
@PostLoad public void postLoad(Object entity) {
}
@PreUpdate public void preUpdate(Object entity) {
}
@PostUpdate public void postUpdate(Object entity) {
}
@PreRemove public void preRemove(Object entity) {
}
@PostRemove public void postRemove(Object entity) {
}
}
Same Listener class may listen to multiple entity types
Good for when Listener has specific purpose that is type-agnostic
JPA and Validation API annotations can be mixed together
@Entity
@Table(name="VALIDATION_PERSON")
public class Person {
@Id @GeneratedValue
private int id;
@Column(name="FIRST_NAME", length=12, nullable=false)
@ValidName(min=1, max=12, regexp="^[a-zA-Z\\ \\-]+$", message="invalid first name")
private String firstName;
@Column(name="LAST_NAME", length=20, nullable=false)
@ValidName(min=1, max=20, regexp="^[a-zA-Z\\ \\-]+$", message="invalid last name")
private String lastName;
@Temporal(TemporalType.DATE)
@NotNull(groups={Drivers.class, POCs.class})
@Past(groups=Drivers.class)
@MinAge.List({
@MinAge(age=18, groups=POCs.class),
@MinAge(age=16, groups=Drivers.class)
})
private Date birthDate;
@Column(name="EMAIL", length=50)
@NotNull(groups=POCs.class)
@Size(min=7,max=50)
@Pattern(regexp="^.+@.+\\..+$")
private String email;
...
<?xml version="1.0" encoding="UTF-8"?>
<persistence ...
<persistence-unit name="jpa-validation-example-test">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<validation-mode>AUTO</validation-mode>
<properties>
<property name="javax.persistence.validation.group.pre-persist"
value="ejava.jpa.example.validation.Drivers"/>
<property name="javax.persistence.validation.group.pre-update"
value="ejava.jpa.example.validation.Drivers"/>
</properties>
</persistence-unit>
</persistence>
validation-mode
AUTO - enable if validator present in classpath
CALLBACK - turn on and report error if no validator found in classpath
NONE - turn off
Can also be set using javax.persistence.validation.mode property
validation groups
javax.persistence.validation.group.pre-persist - defines groups to call during @PrePersist phase
javax.persistence.validation.group.pre-update - defines groups to call during @PreUpdate phase
javax.persistence.validation.group.pre-remove - defines groups to call during @PreRemove phase
default behavior
Default group for @PrePersist and @PreUpdate
Nothing for @PreRemove
Person p = new Person()
.setFirstName("Bob")
.setLastName("Smith")
.setBirthDate(new Date());
try {
em.persist(p);
} catch (ConstraintViolationException ex) {
logger.info("caught expected exception:" + ex);
}
-caught expected exception:javax.validation.ConstraintViolationException: Validation failed for classes [ejava.jpa.example.validation.Person] during persist time for groups [ejava.jpa.example.validation.Drivers, ] List of constraint violations:[ ConstraintViolationImpl{ interpolatedMessage='too young', propertyPath=birthDate, rootBeanClass=class ejava.jpa.example.validation.Person, messageTemplate='too young'} ConstraintViolationImpl{ interpolatedMessage='0 is younger than minimum 16', propertyPath=birthDate, rootBeanClass=class ejava.jpa.example.validation.Person, messageTemplate='0 is younger than minimum 16'} ]
Validation is not automatically cascaded (@Valid) across relationships
Validation will occur for related entity during its appropriate lifecycle phases
Groups could be divided into client, service, DAO-insert, DAO-update, DAO-delete
Permits errors to be automatically detected without transaction going into rollback state
Figure 55.1. API Dependency
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
<scope>compile</scope>
</dependency>
Figure 55.2. Implementation Dependency (includes API dependency)
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.10.Final</version>
<scope>test</scope>
</dependency>
Figure 55.3. Implementation Dependency for @Pattern constraints
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.1-b06</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.1-b06</version>
<scope>test</scope>
</dependency>
Table of Contents
At the completion of this topic, the student shall
have an understanding of:
Object/Relational Mapping (ORM) based on the Java Persistence API (JPA)
Ownership (owning and inverse sides)
Relationship mapping strategies
Cardinality (one-to-one, one-to-many, many-to-one, and many-to-many)
Direction (uni-directional, bi-directional)
Realization (foreign key and link table)
Simple types/element collections
Orphan removal
Cascades
Foreign key reuse (primary key as foreign key (insert/update=false) and foreign key as primary key(@MapsId))
be able to:
Be able to map a Java entity class relationships to the database using class annotations and ORM descriptors
Be able to map one-to-one, one-to-many, many-to-one, and many-to-many relationships
Be able form relationships using foreign keys and link tables
Be able define cascades for relationships
Uni-directional
Only one class ("owner") knows of the relationship
Uses the @OneToOne annotation
Defines mapping to database
Uses either @JoinColumn or @JoinTable
Bi-directional
Both classes know of the relationship
Both classes use the @OneToOne annotation
One class is considered the owner and maps relation to the database
Uses either @JoinColumn or @JoinTable
Changes here change the database
One class is considered the inverse and names the other entity's property
@OneToOne(mappedBy="owning-property")
Changes here do *not* change database
Figure 56.3. One-to-One Uni-directional Example Database Schema
create table ORMREL_PERSON ( PERSON_ID bigint generated by default as identity, firstName varchar(255), lastName varchar(255), phone varchar(255), +----- PERSON_PHOTO bigint, | primary key (PERSON_ID) | ) | create table ORMREL_PHOTO ( +-----> PHOTO_ID bigint generated by default as identity, image blob, primary key (PHOTO_ID) ) alter table ORMREL_PERSON add constraint FK14D7C425DCCB1C0D foreign key (PERSON_PHOTO) references ORMREL_PHOTO
Relation realized through a foreign key
Foreign key represented by a separate column
Foreign key from owning entity table references primary key of inverse entity table
Figure 56.4. One-to-One Uni-directional Example Java Mapping
@Entity
@Table(name="ORMREL_PERSON")
public class Person {
@Id @GeneratedValue @Column(name="PERSON_ID")
private long id;
@OneToOne(cascade={
CascadeType.ALL}, //have creates, deletes, etc. cascade to photo
fetch=FetchType.LAZY) //a hint that we don't need this
@JoinColumn(name="PERSON_PHOTO") //define local foreign key column
+----- private Photo photo;
|
| @Entity
| @Table(name="ORMREL_PHOTO")
| public class Photo {
| @Id @GeneratedValue @Column(name="PHOTO_ID")
+----> private long id;
Figure 56.5. One-to-One Uni-directional Usage
Form the Relationship
//create the owning side
ejava.examples.orm.rel.annotated.Person person = new Person();
person.setFirstName("john");
person.setLastName("doe");
person.setPhone("410-555-1212");
//create the inverse side
ejava.examples.orm.rel.annotated.Photo photo = new Photo();
photo.setImage(image);
//create the person and photo detached
assertEquals(0, person.getId());
assertEquals(0, photo.getId());
//add photo to person and persist object tree
person.setPhoto(photo); //this sets the FK in person
logger.info("added photo to person:{}", person);
em.persist(person);
assertNotEquals("personId not set", 0, person.getId());
assertNotEquals("photoId not set", 0, photo.getId());
logger.info("created person:{}", person);
logger.info(" and photo:{}", photo);
em.flush();
Output
-added photo to person:Person@7ab802f4, id=0, name=john doe, phone=410-555-1212, photo=Photo@608cd501, id=0. image=46080 bytes - call next value for hibernate_sequence - call next value for hibernate_sequence -Photo@608cd501: getId()=2 -created person:Person@7ab802f4, id=1, name=john doe, phone=410-555-1212, photo=Photo@608cd501, id=2. image=46080 bytes - and photo:Photo@608cd501, id=2. image=46080 bytes - insert into ORMREL_PHOTO (image, PHOTO_ID) values (?, ?) - insert into ORMREL_PERSON (firstName, lastName, phone, PERSON_PHOTO, PERSON_ID) values (?, ?, ?, ?, ?)
Person and Photo are given an ID during the persist
Photo is inspected for its ID to be assigned to Person FK to Photo
Rows (with foreign key) are inserted into database on next flush cycle
Find Object with Relationship in Database
//verify what we can get from DB
em.flush(); em.clear();
Person person2 = em.find(Person.class, person.getId());
assertNotNull(person2);
assertNotNull(person2.getPhoto());
logger.info("found person:{}", person2);
Output
- select person0_.PERSON_ID as PERSON_I1_27_0_, person0_.firstName as firstNam2_27_0_, person0_.lastName as lastName3_27_0_, person0_.phone as phone4_27_0_, person0_.PERSON_PHOTO as PERSON_P5_27_0_ from ORMREL_PERSON person0_ where person0_.PERSON_ID=? -Person@20cdb152, ctor() -Photo$HibernateProxy$2MfejJnf@0: ctor() - select photo0_.PHOTO_ID as PHOTO_ID1_28_0_, photo0_.image as image2_28_0_ from ORMREL_PHOTO photo0_ where photo0_.PHOTO_ID=? -Photo@57fdb8a4: ctor() -found person:Person@20cdb152, id=1, name=john doe, phone=410-555-1212, photo=Photo@57fdb8a4, id=2. image=46080 bytes
Calls to em.clear()
for are test purposes only and should not
be a common thing in production code. We must clear the current instance from
the cache if we want the provider to query the database for the rows versus
pulling the existing instance from the cache. Calling em.clear()
within production code will clear all instances from the persistence context
and make your code have unwanted side-effects when called.
optional:boolean (default=true)
Designates whether relation is required. Default is true.
fetch:FetchType (default=EAGER)
Use EAGER or LAZY fetching of relationship when loading this entity. More of coverage of fetch in Fetching section
orphanRemoval:boolean (default=false)
Remote entity only exists for use by this relation. Automatically delete when relation terminated.
cascade:CascadeType[] (default=none)
Perform actions taken on this entity on related entity
targetEntity:Class
Define type for related class (if related Java type over-generalized)
mappedBy:String
Used by inverse side to specify owning entity property that maps relation to DB
Defines a foreign key mapping
name
Database column name
referencedColumnName (default=primary key)
Primarily used during composite key mappings to signify which property goes with which primary key member
unique (default=false)
Foreign key is unique within entity table
nullable (default=true)
Foreign key is optional within entity table. Used for 0..1 relationships
insertable (default=true)
Foreign key is part of SQL INSERT for entity
updatable (default=true)
Foreign key is part of SQL UPDATEs for the entity
table
Used in multi-table mappings to identify source table for column
columnDefinition
Customized DDL for column definition when generating schema
Used to define multiple @JoinColumns when using composite foreign keys
@OneToOne
@JoinColumns({ //defines an array of @JoinColumns
@JoinColumn(...),
@JoinColumn(...)
})
Navigate relationship from both sides of the Java relationship
Figure 56.6. One-to-One Uni-directional Example Database Schema
create table ORMREL_APPLICANT ( id bigint generated by default as identity, +----- APP_BORROWER bigint, | APP_PERSON bigint not null, | primary key (id) | ) | create table ORMREL_BORROWER ( +----> BORROWER_ID bigint not null, endDate date, startDate date, primary key (BORROWER_ID) ) alter table ORMREL_APPLICANT add constraint FKD1860812DA35F52F foreign key (APP_BORROWER) references ORMREL_BORROWER
No additional foreign key used to satisfy the bi-directional aspect of relation
Figure 56.7. One-to-One Uni-directional Example Database Java Mapping
@Entity @Table(name="ORMREL_APPLICANT")
public class Applicant {
@Id @GeneratedValue
private long id;
+--> @OneToOne(optional=true) //we may exist without Borrower
| @JoinColumn(name="APP_BORROWER")//we own relationship to Borrower
+----- private Borrower borrower;
| |
| | @Entity @Table(name="ORMREL_BORROWER")
| | public class Borrower {
| | @Id @Column(name="BORROWER_ID")
`----> private long id;
|
| @OneToOne(fetch=FetchType.LAZY,
| optional=true, //lets make this optional for demo
| mappedBy="borrower") //the other side owns foreign key column
+-- private Applicant application;
Figure 56.8. One-to-One Uni-directional Example Usage
//locate them from DB
Applicant applicant2 = em.find(Applicant.class, applicant.getId());
Borrower borrower2 = em.find(Borrower.class, borrower.getId());
//form relationship
borrower2.setApplication(applicant2); //set inverse side
applicant2.setBorrower(borrower2); //set owning side
update ORMREL_APPLICANT set APP_BORROWER=?, APP_PERSON=? where id=?
Notice only owning entity table is updated when relationship formed.
Figure 56.9. One-to-One Uni-directional Example Usage (cont.)
//locate them from DB
em.flush(); em.clear();
Applicant applicant3 = em.find(Applicant.class, applicant.getId());
Borrower borrower3 = em.find(Borrower.class, borrower.getId());
assertEquals(applicant.getId(), borrower3.getApplication().getId());
assertEquals(borrower.getId(), applicant3.getBorrower().getId());
- select applicant0_.id as id1_12_0_, applicant0_.APP_BORROWER as APP_BORR2_12_0_, applicant0_.APP_PERSON as APP_PERS3_12_0_, borrower1_.BORROWER_ID as BORROWER1_15_1_, borrower1_.endDate as endDate2_15_1_, borrower1_.startDate as startDat3_15_1_, person2_.PERSON_ID as PERSON_I1_27_2_, person2_.firstName as firstNam2_27_2_, person2_.lastName as lastName3_27_2_, person2_.phone as phone4_27_2_, person2_.PERSON_PHOTO as PERSON_P5_27_2_ from ORMREL_APPLICANT applicant0_ left outer join ORMREL_BORROWER borrower1_ on applicant0_.APP_BORROWER=borrower1_.BORROWER_ID inner join ORMREL_PERSON person2_ on applicant0_.APP_PERSON=person2_.PERSON_ID where applicant0_.id=? -Applicant@1a531422, ctor() -Borrower@7a388990, ctor() -Person@13213f26, ctor()
Notice the extra joins that occur with default fetch mode=EAGER
inner joins used for optional=false relationships
left outer joins used for optional=true relationships
Borrower already fetched when obtained Applicant
Only changes made to owning side impact database
persist (set)
update (change)
remove (null out)
Actions taken on owning side not automatically propagated to inverse side
Inverse reference to owning side not changed (for persist, update, or remove)
Must either
Manually update inverse side
Refresh object if proper state in database
Figure 56.10. Update Inverse-side Only Example (Setup)
Borrower borrower = em.find(Borrower.class, borrowerId);
Applicant applicant = em.find(Applicant.class, applicantId);
assertNull("borrower has unexpected applicant:" +
borrower.getApplication(),
borrower.getApplication());
assertNull("applicant has unexpected borrower:" +
applicant.getBorrower(),
applicant.getBorrower());
//set ONLY the inverse side of the relationship
borrower.setApplication(applicant);
assertNotNull("borrower does not have applicant",
borrower.getApplication());
assertNull("applicant has unexpected borrower:" +
applicant.getBorrower(),
applicant.getBorrower());
logger.info("writing rel owner (application) to DB:{}", applicant);
logger.info("writing rel inverse (borrower) to DB:{}", borrower);
Only inverse side being set | |
Setter does not automatically propagate to other side in cache |
Figure 56.11. Update Inverse-side Only Example (Verify)
//commit changes to the DB, but since only inserse side of relationship
//was set, no FK data gets written
em.getTransaction().commit(); em.clear();
assertFalse("borrower was managed", em.contains(borrower));
assertFalse("application was managed", em.contains(applicant));
borrower = em.find(Borrower.class, borrowerId);
applicant = em.find(Applicant.class, applicantId);
//verify that relationship from cache never written to DB
assertNull("borrower has unexpected applicant:" +
borrower.getApplication(),
borrower.getApplication());
assertNull("applicant has unexpected borrower:" +
applicant.getBorrower(),
applicant.getBorrower());
Not setting owning side caused relationship to *not* be written to database -- thus lost when read back in from database |
Figure 56.12. Update Owning-side Only Example (Setup)
Borrower borrower = em.find(Borrower.class, borrowerId);
Applicant applicant = em.find(Applicant.class, applicantId);
assertNull("borrower has unexpected applicant:" +
borrower.getApplication(),
borrower.getApplication());
assertNull("applicant has unexpected borrower:" +
applicant.getBorrower(),
applicant.getBorrower());
//set ONLY the owning side of the relationship
applicant.setBorrower(borrower);
assertNull("borrower has unexpected applicant:" +
borrower.getApplication(),
borrower.getApplication());
assertNotNull("applicant does not have borrower",
applicant.getBorrower());
Updating only the owning side | |
Change does *not* automatically propagate to other side |
Figure 56.13. Update Owning-side Only Example (Verify)
//commit changes to the DB, since the owning side was set, we do
//get changes made to DB
em.getTransaction().commit(); em.clear();
borrower = em.find(Borrower.class, borrowerId);
applicant = em.find(Applicant.class, applicantId);
//verify that relationship from cache written to DB
assertNotNull("borrower was not updated with applicant:" +
borrower.getApplication(),
borrower.getApplication());
assertNotNull("applicant was not updated with borrower",
applicant.getBorrower());
Relationship retained because owning side written to database |
Primary Key Join
No separate foreign key column
Primary key column used to also represent the foreign key
Discussed in Foreign Key section
Link Table
Neither entity class table directly references to other
Separate table used to realize relationship
Discussed in Join Tables section
Represents two classes that reference the other using a Java reference
Relationship realized through either a foreign key or join table
May be represented at the entity level as either a uni-directional or bi-directional relationship
Only the owning side is mapped to the database
Inverse side is populated from database but does not change the database
Uni-directional
Only one side ("owner") knows of the relationship
Uses the @OneToMany annotation
Defines mapping to database
Uses either @JoinColumn or @JoinTable
@JoinTable adds the foreign key to the child table and not to the owning entity class table in this uni-directional case
Bi-directional
Both classes know of the relationship
Many side required to be owning side and maps relation to the database
Uses the @ManyToOne annotation
Uses either @JoinColumn or @JoinTable
Changes here change the database
One side required to be inverse and names the other entity's property
@OneToMany(mappedBy="owning-property")
Changes here do *not* change database
This example uses the @JoinColumn technique of inserting foreign key into child table of inverse entity class
No construct in child/inverse side to map to foreign key
Figure 57.3. One-to-Many Uni-directional Database Schema
create table ORMREL_INVENTORY ( /----> id bigint generated by default as identity, | name varchar(255), | primary key (id) | ) | create table ORMREL_MEDIA ( | MEDIA_ID bigint generated by default as identity, | title varchar(255), `----- INVENTORY_ID bigint, primary key (MEDIA_ID) ) alter table ORMREL_MEDIA add constraint FK639A68F4BCF517CD foreign key (INVENTORY_ID) references ORMREL_INVENTORY
Figure 57.4. One-to-Many Uni-directional Database Java Mapping
@Entity @Table(name="ORMREL_INVENTORY")
public class Inventory {
@Id @GeneratedValue
private long id;
private String name;
@OneToMany(cascade={CascadeType.ALL})
/----- @JoinColumn(name="INVENTORY_ID")
| private Collection<Media> media = new ArrayList<Media>();
|
| @Entity @Table(name="ORMREL_MEDIA")
| public class Media {
| @Id @GeneratedValue @Column(name="MEDIA_ID")
`----> private long id;
Figure 57.5. One-to-Many Uni-directional Database Usage
ejava.examples.orm.rel.annotated.Inventory inventory = new Inventory();
inventory.setName("testLinkCreate");
em.persist(inventory);
for(int i=0; i<5; i++) {
ejava.examples.orm.rel.annotated.Media media = new Media();
em.persist(media);
logger.info("created media: {}", media);
inventory.getMedia().add(media);
}
logger.info("created inventory:{}", inventory);
Relationship formed when inverse side added to owning collection
Figure 57.6. One-to-Many Uni-directional Database Usage
- call next value for hibernate_sequence - call next value for hibernate_sequence -created media:Media@7b222230, id=2, title=null, authors(0)={} - call next value for hibernate_sequence -created media:Media@7df6d663, id=3, title=null, authors(0)={} ... - insert into ORMREL_INVENTORY (name, id) values (?, ?) - insert into ORMREL_MEDIA (title, MEDIA_ID) values (?, ?) - insert into ORMREL_MEDIA (title, MEDIA_ID) values (?, ?) ... -created inventory:Inventory@4ef4f627, id=1, name=testLinkCreate, media(5)={2,3,4,5,6,}
The foreign key is in the inverse entity class table
No construct in inverse class maps to this foreign key column
Identical properties to @OneToOne annotation with the exception of no "optional" property
Figure 57.7. One-to-Many Bi-directional Example Database Schema
create table ORMREL_BORROWER ( +----> BORROWER_ID bigint not null, | endDate date, | startDate date, | primary key (BORROWER_ID) | ) | create table ORMREL_CHECKOUT ( | CHECKOUT_ID bigint generated by default as identity, | outDate date, | returnDate date, `----- CHECKOUT_BID bigint not null, primary key (CHECKOUT_ID) ) alter table ORMREL_CHECKOUT add constraint FK7F287E16C07B41F3 foreign key (CHECKOUT_BID) references ORMREL_BORROWER
Figure 57.8. One-to-Many Bi-directional Example Java Mapping
@Entity @Table(name="ORMREL_CHECKOUT")
public class Checkout {
@Id @GeneratedValue @Column(name="CHECKOUT_ID")
private long id;
+--> @ManyToOne(optional=false)
| @JoinColumn(name="CHECKOUT_BID")
+----- private Borrower borrower;
| |
| | @Entity @Table(name="ORMREL_BORROWER")
| | public class Borrower {
| | private static Log log = LogFactory.getLog(Borrower.class);
| | @Id @Column(name="BORROWER_ID")
`----> private long id;
|
| @OneToMany(mappedBy="borrower", //this relationship is owned by Checkout
| fetch=FetchType.LAZY) //try to limit what we get back
`--- private Collection<Checkout> checkouts = new ArrayList<Checkout>();
Figure 57.9. One-to-Many Bi-directional Example Usage (Create)
//get a borrower
Borrower borrower = em.find(Borrower.class, borrowerId);
assertNotNull(borrower);
assertEquals(0, borrower.getCheckouts().size());
//create 1st checkout
Checkout checkout = new Checkout(new Date());
checkout.setBorrower(borrower); //set owning side of the relation
//wrapper around - borrower.getCheckouts().add(checkout)
borrower.addCheckout(checkout); //set inverse side of relation
em.persist(checkout); //persist owning side of the relation
em.flush();
-getting borrower id=1 - select borrower0_.BORROWER_ID as BORROWER1_15_0_, borrower0_.endDate as endDate2_15_0_, borrower0_.startDate as startDat3_15_0_ from ORMREL_BORROWER borrower0_ where borrower0_.BORROWER_ID=? ... - call next value for hibernate_sequence - insert into ORMREL_CHECKOUT (CHECKOUT_BID, outDate, returnDate, CHECKOUT_ID) values (?, ?, ?, ?)
//get a borrower
//create a couple more
for(int i=0; i<5; i++) {
Checkout co = new Checkout(new Date());
co.setBorrower(borrower); //set owning side of the relation
borrower.addCheckout(co); //set inverse side of relation
em.persist(co); //persist owning side of the relation
}
em.flush();
- call next value for hibernate_sequence ... - insert into ORMREL_CHECKOUT (CHECKOUT_BID, outDate, returnDate, CHECKOUT_ID) values (?, ?, ?, ?) ...
Figure 57.10. One-to-Many Bi-directional Example Usage (Verify)
//check the DB
em.flush(); em.clear();
Borrower borrower2 = em.find(Borrower.class, borrower.getId());
assertEquals(6, borrower2.getCheckouts().size());
Maps collection of simple data types to child table
Uses orphanRemoval for child values
Figure 57.11. Element Collection Example Database Schema
create table RELATIONEX_SUSPECT ( +----> id integer generated by default as identity, | name varchar(32), | primary key (id) | ) | create table RELATIONEX_SUSPECT_ALIASES ( `----- SUSPECT_ID integer not null, ALIAS varchar(32), unique (SUSPECT_ID, ALIAS) ) alter table RELATIONEX_SUSPECT_ALIASES add constraint FK3FD160E6DE29C9CF foreign key (SUSPECT_ID) references RELATIONEX_SUSPECT
Figure 57.12. Element Collection Example Database Java Mapping
@Entity
@Table(name="RELATIONEX_SUSPECT")
public class Suspect {
@Id @GeneratedValue
private int id;
@Column(length=32)
private String name;
@ElementCollection
@CollectionTable(
name="RELATIONEX_SUSPECT_ALIASES",
joinColumns=@JoinColumn(name="SUSPECT_ID"),
uniqueConstraints=@UniqueConstraint(columnNames={"SUSPECT_ID", "ALIAS"}))
@Column(name="ALIAS", length=32)
private Set<String> aliases;
Figure 57.13. Element Collection Example Database Usage
Suspect suspect = new Suspect(); suspect.setName("william"); em.persist(suspect); suspect.getAliases().add("bill"); suspect.getAliases().add("billy");
Link Table
Foreign key not placed in child table
Separate table used to realize relationship
Discussed in Join Tables section
Determines how when related objects are retrieved
LAZY
Only parent object immediately retrieved -- child objects retrieved on demand
Can result in better performance when child data is not needed - unused objects not retrieved
Can result in poor performance when all child data is needed - retrieves objects one at a time rather than in bulk
Can result in lazy load exception if unloaded data accessed after it can no longer be retrieved from database
Can be simulated, on demand, using value or result class queries with JPA-QL
EAGER (the default)
Provider required to have loaded prior to transaction committing
Can result in better performance when all child data is needed
Can result in poor performance when no child data or limited child data is needed
Can result in poor performance when parent has multiple collections and equivalent of an "EAGER LAZY load" is performed before returning control back to the caller
Should never result in a lazy load exception
Can be simulated, on-demand, using "join fetch" queries with JPA-QL
Figure 58.1. fetch=LAZY Example Declaration
@OneToMany(mappedBy="borrower", //this relationship is owned by Checkout
fetch=FetchType.LAZY)
private Collection<Checkout> checkouts = new ArrayList<Checkout>();
@XxxToXxx Relationship annotated with fetch=LAZY property
Figure 58.2. fetch=LAZY Example Use
Borrower borrower2 = em.find(Borrower.class, borrower.getId());
logger.info("found borrower: {}", borrower.getName());
assertEquals(6, borrower2.getCheckouts().size());
Parent accessed, debug printed, and then child collection accessed
Figure 58.3. fetch=LAZY SQL Output
- select borrower0_.BORROWER_ID as BORROWER1_15_0_, borrower0_.endDate as endDate2_15_0_, borrower0_.startDate as startDat3_15_0_ from ORMREL_BORROWER borrower0_ where borrower0_.BORROWER_ID=? -Borrower@7cfb0c4c, ctor() ... -found borrower: john smith - select checkouts0_.CHECKOUT_BID as CHECKOUT4_16_0_, checkouts0_.CHECKOUT_ID as CHECKOUT1_16_0_, checkouts0_.CHECKOUT_ID as CHECKOUT1_16_1_, checkouts0_.CHECKOUT_BID as CHECKOUT4_16_1_, checkouts0_.outDate as outDate2_16_1_, checkouts0_.returnDate as returnDa3_16_1_ from ORMREL_CHECKOUT checkouts0_ where checkouts0_.CHECKOUT_BID=?
Debug for parent printed before child rows retrieved
Child rows retrieved when parent collection accessed
Other SQL details of Borrower left out of example
Figure 58.4. fetch=EAGER Example Declaration
@OneToMany(mappedBy="borrower", //this relationship is owned by Checkout
fetch=FetchType.EAGER)
private Collection<Checkout> checkouts = new ArrayList<Checkout>();
Figure 58.5. fetch=EAGER Example Use
Borrower borrower3 = em.find(Borrower.class, borrowerId);
logger.info("found borrower: {}", borrower.getName());
assertEquals(0,borrower3.getCheckouts().size());
Same as fetch=LAZY case
Figure 58.6. fetch=EAGER SQL Output
- select borrower0_.BORROWER_ID as BORROWER1_15_0_, borrower0_.endDate as endDate2_15_0_, borrower0_.startDate as startDat3_15_0_, checkouts1_.CHECKOUT_BID as CHECKOUT4_16_1_, checkouts1_.CHECKOUT_ID as CHECKOUT1_16_1_, checkouts1_.CHECKOUT_ID as CHECKOUT1_16_2_, checkouts1_.CHECKOUT_BID as CHECKOUT4_16_2_, checkouts1_.outDate as outDate2_16_2_, checkouts1_.returnDate as returnDa3_16_2_ from ORMREL_BORROWER borrower0_ left outer join ORMREL_CHECKOUT checkouts1_ on borrower0_.BORROWER_ID=checkouts1_.CHECKOUT_BID where borrower0_.BORROWER_ID=? -found borrower: john smith
Child objects fetched with parent
Access to child collection occurs after all children fetched
fetch=EAGER is only a promise to fetch the related object before returning from the call. It does not always mean that a SQL JOIN was performed. There are times (e.g., fetch=EAGER on multiple collections within parent) when the provider will perform a SQL JOIN for some of the relationships and the functional equivalent of a fetch=LAZY for the remaining relationships.
Although useful at times, avoid haphazard use of fetch=EAGER when defining relationships and look to rely on custom queries to do this behavior instead. It is very easy to turn a fetch=LAZY into EAGER at query time. It is very difficult to do the opposite. Custom queries also allow us to incrementally build a complex parent object tree an optimized query at a time. fetch=EAGER will do the same, but may perform 10s, 100s, 1000s of more unoptimized queries of child rows we may never need.
Automatically cause persistence commands to be repeated on related objects
Figure 58.7.
@Entity
@Table(name="RELATIONEX_LICAPP")
public class LicenseApplication {
@Id @GeneratedValue
private int id;
@Temporal(TemporalType.TIMESTAMP)
private Date updated;
@OneToOne(optional=false, fetch=FetchType.EAGER,
cascade={
CascadeType.PERSIST,
CascadeType.DETACH,
CascadeType.REMOVE,
CascadeType.REFRESH,
CascadeType.MERGE
})
private License license;
PERSIST
Related entities persisted when this entity is passed to em.persist()
DETACH
Related entities detached from persistence unit when this entity passed to em.detach()
REMOVE
Related entities deleted from database when this entity passed to em.remove()
REFRESH
Related entities refreshed with state of database when this entity passed to em.refresh()
MERGE
Related entities update state of database when this entity is passed to em.merge()
Automatic removal of an object who's sole purpose is to support a related object that may dereference it
Related to cascade=REMOVE but the triggering object is not being deleted
Supported in the following relationships
@OneToOne
@OneToMany
Figure 58.8. OrphanRemoval Example Declaration
@Entity
@Table(name="RELATIONEX_ATTENDEE")
public class Attendee {
@Id @GeneratedValue
private int id;
//orphanRemoval will take care of dereference and DELETE from dependent Attendee
@OneToOne(cascade=CascadeType.PERSIST, orphanRemoval=true)
private Residence residence;
Figure 58.10. OrphanRemoval Database Interaction
Hibernate: update RELATIONEX_ATTENDEE set name=?, residence_id=? where id=? Hibernate: delete from RELATIONEX_RESIDENCE where id=?
Attendee.residence_id set to null
Orphaned residence deleted
Use fetch=EAGER when always accessing related objects together
Use fetch=LAZY when commonly access one object without accessing related objects
Use JPA-QL when encountering corner cases that violate default mapping
Cascades can be used to automate persistence actions on an entire object graph
OrphanRemoval can used to automatically delete dereferenced objects that have no use outside the scope of its owning relation
hashCode()
Returns an int value to be used by hashtables to help store and locate
Must remain constant *unless* state that is used to derive value changes
Two objects resulting in equals(obj)=true must return same hashCode
Two objects resulting in same hashCode may result in equals(obj)=false - but may lead to inneficient access
equals()
Returns the boolean result of whether two objects are "equal"
Reflexive - this should always equal this
Symmetric - if x==y, then y==x
Transitive - if x==y and y==z, then x==z
Consistent - if x==y, then x will always equal y (unless state used changes)
Uses object instance to determine identity
Two instances with identical state will not be equal
Default implementation
Works for cases where you will only have a single instance representing a single object
Figure 59.1. Using Default Identity Methods
public class java.lang.Object {
...
public native int hashCode();
public boolean equals(java.lang.Object);
...
}
Use assigned database primary key as identity
Positives: unique value within table or possibly database
Negatives: automatically generated values not available until object persisted
Only an issue for auto-generated primary keys
Figure 59.2. Using Database Primary Key For Identity
@Entity
public abstract class Ship {
@Id
@GeneratedValue
protected int id;
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(Object obj) {
try {
if (obj == null) { return false; }
if (this == obj) { return true; }
return id==((Ship)obj).id;
} catch (Exception ex) { return false; }
}
Using instanceId until database primary key assigned
Positives: able to compare objects in same persistent stage
Negatives:
Persisting object will break consistency rule (except that state really did change)
Cannot compare transient instances to persisted instances
Identity-based collections can get confused by inconsistent behavior
Figure 59.3. Switching Identity Schemes
@Override
public int hashCode() {
return id==0 ? super.hashCode() : id;
}
@Override
public boolean equals(Object obj) {
try {
if (obj == null) return false;
if (this == obj) { return true; }
return (id==0) ? super.equals(obj) : id==((Ship)obj).id;
} catch (Exception ex) { return false; }
}
Use unique business values to determine identity -- independent of database primary key
Positives: consistent state through persistence lifecycle of object
Negatives: may have trouble identifying unique properties within each object type
Figure 59.4. Switching Identity Schemes
@Override
public int hashCode() {
return (name==null ? 0 : name.hashCode()) +
(created==null ? 0 : (int)created.getTime());
}
@Override
public boolean equals(Object obj) {
try {
if (obj == null) { return false; }
if (this == obj) { return true; }
return name.equals(((ShipByBusinessId)obj).name) &&
created.getTime() == (((ShipByBusinessId)obj).created.getTime());
} catch (Exception ex) { return false; }
}
Collection
Bag, no specific collection ordering. Duplicates allowed.
List
Duplicates allowed. Order maintained by database using @SortKey("property ASC/DESC")
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER) @JoinColumn @OrderBy("number ASC") private List<Segment> segments;
Set
Unique value (object identity comes into play here). No specific order.
Map
Each object registered in collection with a property key using @MapKey("property")
@OneToMany @MapKey(name="position") @JoinColumn(name="LINEUP_ID") private Map<String, Position> positions;
Special case of one-to-one mapping
Uses primary key as the foreign key
No additional column for foreign key
Primary keys must match
Figure 60.2. Primary Key Join Example Database Schema
create table ORMREL_BORROWER ( +----- BORROWER_ID bigint not null, | endDate date, | startDate date, | primary key (BORROWER_ID) | ) | create table ORMREL_PERSON ( +----> PERSON_ID bigint generated by default as identity, firstName varchar(255), lastName varchar(255), phone varchar(255), PERSON_PHOTO bigint, primary key (PERSON_ID) ) alter table ORMREL_BORROWER add constraint FKA0973E32F113D9BA foreign key (BORROWER_ID) references ORMREL_PERSON
Figure 60.3. Primary Key Join Example Java Mapping
@Entity @Table(name="ORMREL_BORROWER")
public class Borrower {
@Id @Column(name="BORROWER_ID")
+----- private long id;
|
| @OneToOne(fetch=FetchType.LAZY, optional=false,
| cascade={CascadeType.PERSIST,
| CascadeType.REFRESH,
| CascadeType.MERGE})
| @PrimaryKeyJoinColumn //the two tables will be joined by PKs
+----- private Person identity;
|
| @Entity
| @Table(name="ORMREL_PERSON")
| public class Person {
| @Id @GeneratedValue @Column(name="PERSON_ID")
+----> private long id;
public Borrower(Person identity) {
this.id = identity.getId();
this.identity = identity;
}
In this case, the primary key is used as the foreign key and must be set. To use the foreign key as the primary key -- use @MapsId
//@PrimaryKeyJoinColumn //the two tables will be joined by PKs @MapsId private Person identity; public Borrower(Person identity) { this.identity = identity; }
Figure 60.4. Primary Key Join Example Usage
//create the person we'll use in the relationship
Person person = new Person();
...
//create the Borrower, who requires a Person for its identity
Borrower borrower = new Borrower(person);
borrower.setStartDate(new Date());
//persist the borrower, creating the relationship to person
em.persist(borrower);
logger.info("created borrower: {}", borrower);
assertEquals(person.getId(), borrower.getId()); //ctor copies PK
-Person@61f377d1, ctor()
-
call next value for hibernate_sequence
-created person:Person@61f377d1, id=1, name=jerome doe, phone=410-555-1212, photo=null
-Borrower@27960b1e, ctor():
-created borrower:Borrower,
id=1, startDate=Sat Aug 18 12:00:37 EDT 2018, endDate=null,
identity=Person@61f377d1, id=1, name=jerome doe, phone=410-555-1212, photo=null, applicant=null, checkouts={}
The identity is determined for Person from the database
The identity of Borrower is obtained from Person at some point prior to the next flush cycle
The identity of Person and Borrower will always be the same in a PrimaryKeyJoin
Since Borrower requires a primary key and the column is also a foreign key to Person -- Borrower must always have a Person
Signals primary key column used as foreign key -- no separate foreign key column
name (default=primary key column of this entity)
Names column in this entity's table this property maps to when using composite keys
referencedColumnName (default=primary key column of joined entity)
Names column in joined entity table this property maps to when using composite keys
columnDefinition
Custom DDL definition for column when generating database schema
Child object on many side, using a composite primary key may reference parent with a property from that key
Primary keys cannot be changed
If primary key used -- database manipulation for foreign keys reasons must be turned off
Primary key used to represent foreign key
Primary key must be known -- cannot be derived from generated value
Figure 60.5. Composite @IdClass Property Reused for Foreign Key
@Entity @Table(name="ORMREL_ROOM")
@IdClass(RoomPK.class)
@AttributeOverrides({
@AttributeOverride(name = "houseId", column=@Column(name="HOUSE_ID")),
@AttributeOverride(name = "roomId", column=@Column(name="ROOM_ID"))
})
public class Room {
@Id
private int houseId;
@Id
private int roomId;
@ManyToOne(fetch=FetchType.LAZY, optional=false)
//assign join column to primary key value and turn off inserts/updates here
@JoinColumn(name="HOUSE_ID", insertable=false, updatable=false)
private House house;
public Room() {}
public Room(House house, int roomId) {
this.houseId=house.getId();
this.house=house;
this.roomId=roomId;
}
Setting primary key prior to persisting
Foreign key used twice -- once for relation and once for primary key
Figure 60.6. Referenced Parent Object -- Source of Foreign Key/Primary Key Value
@Entity @Table(name="ORMREL_HOUSE")
public class House {
@Id @GeneratedValue
private int id;
@OneToMany(cascade={CascadeType.PERSIST, CascadeType.REMOVE},
fetch=FetchType.LAZY, mappedBy="house")
private Collection<Room> rooms=new ArrayList<Room>();
Figure 60.7. Composite @IdClass
public class RoomPK implements Serializable {
@Column(name="PK_HOUSE_ID") //overridden
private int houseId;
@Column(name="PK_ROOM_ID") //overridden
private int roomId;
Figure 60.8. Database Schema
create table ORMREL_HOUSE ( id integer generated by default as identity, primary key (id) ) create table ORMREL_ROOM ( HOUSE_ID integer not null, ROOM_ID integer not null, primary key (HOUSE_ID, ROOM_ID) ) alter table ORMREL_ROOM add constraint FKD9EEA1ABC0069D7C foreign key (HOUSE_ID) references ORMREL_HOUSE
HOUSE_ID is both primary and foreign key for ROOM
Figure 60.9. Example Use
House house = new House();
em.persist(house); //generate a PK for parent
house.getRooms().add(new Room(house,0));
house.getRooms().add(new Room(house,1));
house.getRooms().add(new Room(house,2));
em.persist(house); //cascade persists to children
//get a new copy of house
em.flush(); em.clear();
House house2 = em.find(House.class, house.getId());
Figure 60.10. Example Output
Hibernate: insert into ORMREL_HOUSE (id) values (null) Hibernate: insert into ORMREL_ROOM (HOUSE_ID, ROOM_ID) values (?, ?) ... Hibernate: select house0_.id as id1_18_0_ from ORMREL_HOUSE house0_ where house0_.id=? Hibernate: select rooms0_.HOUSE_ID as HOUSE1_18_1_, rooms0_.HOUSE_ID as HOUSE1_30_1_, rooms0_.ROOM_ID as ROOM2_30_1_, rooms0_.HOUSE_ID as HOUSE1_30_0_, rooms0_.ROOM_ID as ROOM2_30_0_ from ORMREL_ROOM rooms0_ where rooms0_.HOUSE_ID=?
Primary key used to represent foreign key
Primary key must be known -- cannot be derived from generated value
Figure 60.11. Composite @EmbeddedId Property Reused for Foreign Key
@Entity @Table(name="ORMREL_DOOR")
public class Door {
@EmbeddedId
@AttributeOverrides({
@AttributeOverride(name="houseId", column=@Column(name="HOUSE_ID")),
@AttributeOverride(name="doorId", column=@Column(name="DOOR_ID"))
})
private DoorPK pk;
@ManyToOne(fetch=FetchType.LAZY, optional=false)
//assign join column to primary key value and turn off inserts/updates here
@JoinColumn(name="HOUSE_ID", insertable=false, updatable=false)
private House house;
public Door() {}
public Door(House house, int doorId) {
pk=new DoorPK(house.getId(), doorId);
this.house=house;
}
Figure 60.12. Composite @Embeddable Class
@Embeddable
public class DoorPK implements Serializable {
@Column(name="PK_HOUSE_ID") //overridden
private int houseId;
@Column(name="PK_DOOR_ID") //overridden
private int doorId;
Figure 60.13. Database Schema
create table ORMREL_DOOR ( DOOR_ID integer not null, HOUSE_ID integer not null, primary key (DOOR_ID, HOUSE_ID) ) alter table ORMREL_DOOR add constraint FKD9E8447EC0069D7C foreign key (HOUSE_ID) references ORMREL_HOUSE
Figure 60.14. Example Use
House house = new House();
em.persist(house); //generate a PK for parent
house.getDoors().add(new Door(house,0));
house.getDoors().add(new Door(house,1));
house.getDoors().add(new Door(house,2));
em.persist(house); //cascade persists to children
//get a new copy of house
em.flush(); em.clear();
House house2 = em.find(House.class, house.getId());
Figure 60.15. Example Output
Hibernate: insert into ORMREL_HOUSE (id) values (null) Hibernate: insert into ORMREL_DOOR (DOOR_ID, HOUSE_ID) values (?, ?) ... Hibernate: select house0_.id as id1_18_0_ from ORMREL_HOUSE house0_ where house0_.id=? Hibernate: select doors0_.HOUSE_ID as HOUSE2_18_1_, doors0_.DOOR_ID as DOOR1_17_1_, doors0_.HOUSE_ID as HOUSE2_17_1_, doors0_.DOOR_ID as DOOR1_17_0_, doors0_.HOUSE_ID as HOUSE2_17_0_ from ORMREL_DOOR doors0_ where doors0_.HOUSE_ID=?
Primary key value is derived from foreign key value
Value can be dynamically generated
Figure 60.16. Composite @IdClass Property Derived from Foreign Key
@Entity @Table(name="ORMREL_RESIDENT")
@IdClass(ResidentPK.class)
@AttributeOverrides({
@AttributeOverride(name = "residentId", column=@Column(name="RESIDENT_ID"))
})
public class Resident {
@Id
@ManyToOne(fetch=FetchType.LAZY, optional=false)
@JoinColumn(name="HOUSE_ID", nullable=false)
private House house;
@Id
private int residentId;
public Resident() {}
public Resident(House house, int residentId) {
this.house=house;
this.residentId=residentId;
}
No longer modeling separate primary key - derived from foreign key
Figure 60.17. Composite @IdClass Class
public class ResidentPK implements Serializable {
private int house;
private int residentId;
Figure 60.18. Database Schema
create table ORMREL_RESIDENT ( HOUSE_ID integer not null, RESIDENT_ID integer not null, primary key (HOUSE_ID, RESIDENT_ID) ) alter table ORMREL_RESIDENT add constraint FKEDC7E20C0069D7C foreign key (HOUSE_ID) references ORMREL_HOUSE
Figure 60.19. Example Use
House house = new House();
em.persist(house); //generate a PK for parent
house.getResidents().add(new Resident(house,0));
house.getResidents().add(new Resident(house,1));
house.getResidents().add(new Resident(house,2));
em.persist(house); //cascade persists to children
//get a new copy of house
em.flush(); em.clear();
House house2 = em.find(House.class, house.getId());
Figure 60.20. Example Output
Hibernate: insert into ORMREL_HOUSE (id) values (null) Hibernate: insert into ORMREL_RESIDENT (HOUSE_ID, RESIDENT_ID) values (?, ?) ... Hibernate: select house0_.id as id1_18_0_ from ORMREL_HOUSE house0_ where house0_.id=? Hibernate: select residents0_.HOUSE_ID as HOUSE1_18_1_, residents0_.HOUSE_ID as HOUSE1_29_1_, residents0_.RESIDENT_ID as RESIDENT2_29_1_, residents0_.HOUSE_ID as HOUSE1_29_0_, residents0_.RESIDENT_ID as RESIDENT2_29_0_ from ORMREL_RESIDENT residents0_ where residents0_.HOUSE_ID=?
Figure 60.21. Composite @EmbeddedId Property Derived from Foreign Key
@Entity @Table(name="ORMREL_MORTGAGE")
public class Mortgage {
@EmbeddedId
@AttributeOverrides({
@AttributeOverride(name="mortgageId", column=@Column(name="MORTGAGE_ID"))
})
private MortgagePK pk;
@ManyToOne(fetch=FetchType.LAZY, optional=false)
@JoinColumn(name="HOUSE_ID", nullable=false)
@MapsId("houseId")
private House house;
public Mortgage() {}
public Mortgage(House house, int mortgageId) {
pk=new MortgagePK(house.getId(), mortgageId);
this.house=house;
}
Figure 60.22. Composite @Embeddable Class
@Embeddable
public class MortgagePK implements Serializable {
@Column(name="PK_HOUSE_ID") //overridden
private int houseId;
@Column(name="PK_MORTGAGE_ID") //overridden
private int mortgageId;
Figure 60.23. Database Schema
create table ORMREL_MORTGAGE ( HOUSE_ID integer not null, MORTGAGE_ID integer not null, primary key (HOUSE_ID, MORTGAGE_ID) ) alter table ORMREL_MORTGAGE add constraint FK175C656CC0069D7C foreign key (HOUSE_ID) references ORMREL_HOUSE
Figure 60.24. Example Use
House house = new House();
em.persist(house); //generate a PK for parent
house.getMortgages().add(new Mortgage(house,0));
house.getMortgages().add(new Mortgage(house,1));
house.getMortgages().add(new Mortgage(house,2));
em.persist(house); //cascade persists to children
//get a new copy of house
em.flush(); em.clear();
House house2 = em.find(House.class, house.getId());
Figure 60.25. Example Output
Hibernate: insert into ORMREL_HOUSE (id) values (null) Hibernate: insert into ORMREL_MORTGAGE (HOUSE_ID, MORTGAGE_ID) values (?, ?) ... Hibernate: select house0_.id as id1_18_0_ from ORMREL_HOUSE house0_ where house0_.id=? Hibernate: select mortgages0_.HOUSE_ID as HOUSE1_18_1_, mortgages0_.HOUSE_ID as HOUSE1_26_1_, mortgages0_.MORTGAGE_ID as MORTGAGE2_26_1_, mortgages0_.HOUSE_ID as HOUSE1_26_0_, mortgages0_.MORTGAGE_ID as MORTGAGE2_26_0_ from ORMREL_MORTGAGE mortgages0_ where mortgages0_.HOUSE_ID=?
Separate table created to hold foreign keys
Can be used for all relationship enumatations and directions
Figure 60.27. Join/Link Table Database Table Schema
create table ORMREL_INVENTORY (
/----> id bigint generated by default as identity,
| name varchar(255),
| primary key (id)
| )
| create table ORMREL_INVENTORY_MEDIA (
`----- ORMREL_INVENTORY_id bigint not null,
/----- media_MEDIA_ID bigint not null
| )
| create table ORMREL_MEDIA (
`----> MEDIA_ID bigint generated by default as identity,
title varchar(255),
primary key (MEDIA_ID)
)
Join table name either derived from associated tables or explicitly named
Join table columns either derived from referenced table column or explicitly named
Figure 60.28. Join/Link Table Database Constraint Schema
alter table ORMREL_INVENTORY_MEDIA add constraint UK_F6FA5C31B7DAA951 unique (media_MEDIA_ID) alter table ORMREL_INVENTORY_MEDIA add constraint FKF6FA5C31A70D4E48 foreign key (media_MEDIA_ID) references ORMREL_MEDIA alter table ORMREL_INVENTORY_MEDIA add constraint FKF6FA5C317DD5E49D foreign key (ORMREL_INVENTORY_id) references ORMREL_INVENTORY
Unique constraint enforces the (1)-to-Many aspect of relationship
Figure 60.29. Join/Link Table Database Java Mapping
@Entity @Table(name="ORMREL_INVENTORY")
public class Inventory {
@Id @GeneratedValue
private long id;
private String name;
@OneToMany(cascade={CascadeType.ALL})
@JoinTable(name="ORMREL_INVENTORY_MEDIA")
/----- private Collection<Media> media = new ArrayList<Media>();
|
| @Entity @Table(name="ORMREL_MEDIA")
| public class Media {
| @Id @GeneratedValue @Column(name="MEDIA_ID")
`----> private long id;
name
Database table name for join table
catalog
Database catalog for join table
schema
Database schema for join table
joinColumns
List of @JoinColumn definitions from join table back to owning entity class table
inverseJoinColumns
List of @JoinColumn definitions from join table to inverse entity class table
uniqueConstraints
Uniqueness constraints to be added to join table
Uni-directional
Only one side ("owner") knows of the relationship
Uses the @ManyToOne annotation
Defines mapping to database
Uses either @JoinColumn or @JoinTable
Bi-directional
Same as One-to-Many Bi-directional
Example uses composite key and derives primary key from foreign key
Foreign key auto-generated
Figure 61.2. Many-to-One Uni-directional Database Schema
create table ORMREL_MEDIA ( +--------> MEDIA_ID bigint generated by default as identity, | title varchar(255), | primary key (MEDIA_ID) | ) | create table ORMREL_MEDIACOPY ( | COPY_NO integer not null, `--------- MEDIACOPY_MID bigint not null, primary key (COPY_NO, MEDIACOPY_MID) ) alter table ORMREL_MEDIACOPY add constraint FKCDB47669F152B359 foreign key (MEDIACOPY_MID) references ORMREL_MEDIA
Figure 61.3. Many-to-One Uni-directional Database Java Mapping
@Entity @Table(name="ORMREL_MEDIACOPY2")
@IdClass(MediaCopyPK2.class)
@AttributeOverrides({
@AttributeOverride(name="copyNo", column=@Column(name="COPY_NO"))
})
public class MediaCopy2 {
@Id //mapped to COPY_NO by attribute override
private int copyNo;
@Id
@ManyToOne
@JoinColumn(name="MEDIACOPY_MID")
+----- private Media media;
|
| private MediaCopy2() {}
| public MediaCopy2(Media media, int copyNo) {
| this.media=media;
| this.copyNo=copyNo;
| }
| ...
| @Entity @Table(name="ORMREL_MEDIA")
| public class Media {
`----> @Id @GeneratedValue @Column(name="MEDIA_ID")
private long id;
Figure 61.4. Many-to-One Uni-directional Database Usage
ejava.examples.orm.rel.annotated.Media media = new Media();
media.setTitle("EJB31");
//add media to DB
assertTrue(media.getId() == 0);
em.persist(media);
log.info("created media:" + media);
//create some copies
for(int i=0; i<5; i++) {
ejava.examples.orm.rel.annotated.MediaCopy2 mc =
new MediaCopy2(media, i);
assertNotNull(mc.getMedia());
assertEquals(i, mc.getCopyNo());
em.persist(mc);
log.info("created copy:" + mc);
}
Figure 61.5. Many-to-One Uni-directional Database Output
Hibernate: insert into ORMREL_MEDIA (MEDIA_ID, title) values (null, ?) -created media:Media@51942b40, id=1, title=EJB31, authors(0)={} -created copy:MediaCopy2@3787f275, mediaId=1, copyNo=0, media=Media@51942b40, id=1, title=EJB31, authors(0)={} ... -created copy:MediaCopy2@7a7fdbb0, mediaId=1, copyNo=4, media=Media@51942b40, id=1, title=EJB31, authors(0)={} Hibernate: insert into ORMREL_MEDIACOPY2 (COPY_NO, MEDIACOPY_MID) values (?, ?) ... Hibernate: insert into ORMREL_MEDIACOPY2 (COPY_NO, MEDIACOPY_MID) values (?, ?)
Uni-directional
Only one side ("owner") knows of the relationship
Uses the @ManyToMany annotation
Defines mapping to database
Must use @JoinTable
Bi-directional
Both classes know of the relationship
One side required to be owning side and maps relation to the database
Uses the @ManyToMany annotation
Must use @JoinTable
Changes here change the database
One side required to be inverse and names the other entity's property
@ManyToMany(mappedBy="owning-property")
Changes here do *not* change database
Figure 62.3. Many-to-Many Uni-directional Database Schema
create table ORMREL_WANTED ( +----> id bigint generated by default as identity, | primary key (id) | ) | create table ORMREL_WANTED_MEDIA ( `----- ORMREL_WANTED_id bigint not null, +----- media_MEDIA_ID bigint not null | ) | create table ORMREL_MEDIA ( `----> MEDIA_ID bigint generated by default as identity, title varchar(255), primary key (MEDIA_ID) ) alter table ORMREL_WANTED_MEDIA add constraint FKEE528304A70D4E48 foreign key (media_MEDIA_ID) references ORMREL_MEDIA alter table ORMREL_WANTED_MEDIA add constraint FKEE52830486AFC6B6 foreign key (ORMREL_WANTED_id) references ORMREL_WANTED
Figure 62.4. Many-to-Many Uni-directional Database Java Mapping
@Entity @Table(name="ORMREL_WANTED")
public class WantList {
@Id @GeneratedValue
private long id;
@ManyToMany
@JoinTable(name="ORMREL_WANTED_MEDIA") //define table, let columns use default names
+----- private Collection<Media> media = new ArrayList<Media>();
| ...
| @Entity @Table(name="ORMREL_MEDIA")
| public class Media {
| @Id @GeneratedValue @Column(name="MEDIA_ID")
`----> private long id;
private String title;
Figure 62.5. Many-to-Many Uni-directional Example Usage
for(WantList w: wantLists) {
for(Media m: media) {
//we can only navigate this in one direction
w.getMedia().add(m);
logger.info("added media({}) to want list ({})", m.getId(), w.getId());
}
}
Figure 62.6. Many-to-Many Uni-directional Database Output
-added media(1) to want list (1) -added media(2) to want list (1) -added media(1) to want list (2) -added media(2) to want list (2) -added media(1) to want list (3) -added media(2) to want list (3) Hibernate: insert into ORMREL_WANTED_MEDIA (ORMREL_WANTED_id, media_MEDIA_ID) values (?, ?) ...
Rows added to join table rather than updating entity class tables with foreign key
Figure 62.7. Many-to-Many Bi-directional Database Schema
create table ORMREL_AUTHOR ( +-----> id bigint generated by default as identity, | name varchar(255), | primary key (id) | ) | create table ORMREL_AUTHOR_MEDIA ( `----- LINK_AUTHOR_ID bigint not null, +----- LINK_MEDIA_ID bigint not null | ) | create table ORMREL_MEDIA ( `----> MEDIA_ID bigint generated by default as identity, title varchar(255), primary key (MEDIA_ID) ) alter table ORMREL_AUTHOR_MEDIA add constraint FKA0D2B4E09B01C6F2 foreign key (LINK_MEDIA_ID) references ORMREL_MEDIA alter table ORMREL_AUTHOR_MEDIA add constraint FKA0D2B4E089FFE922 foreign key (LINK_AUTHOR_ID) references ORMREL_AUTHOR
Figure 62.8. Many-to-Many Bi-directional Database Java Mapping
@Entity @Table(name="ORMREL_AUTHOR")
public class Author {
@Id @GeneratedValue
private long id;
private String name;
@ManyToMany
@JoinTable(name="ORMREL_AUTHOR_MEDIA", //defines the link table
//defines the column in the link table for author FK
joinColumns={@JoinColumn(name="LINK_AUTHOR_ID")},
//defines the column in the link table for the media FK
inverseJoinColumns={@JoinColumn(name="LINK_MEDIA_ID")})
+--> @OrderBy("title DESC") //order the list returned from database
+----- private List<Media> media = new ArrayList<Media>();
| |
| | @Entity @Table(name="ORMREL_MEDIA")
| | public class Media {
| | @Id @GeneratedValue @Column(name="MEDIA_ID")
| | private long id;
| | private String title;
| `---- @ManyToMany(mappedBy="media") //names property in Author that points to us
`-----> private Collection<Author> authors = new ArrayList<Author>();
Figure 62.9. Many-to-Many Bi-directional Example Usage
for(Author a: authors) {
for(Media m: media) {
a.getMedia().add(m);
logger.info("added media({}) to author ({})", m.getId(), a.getId());
m.getAuthors().add(a);
logger.info("added author({}) to media ({})", a.getId(), m.getId());
}
}
Owning and inverse sides now both need to be set
Figure 62.10. Many-to-Many Bi-directional Database Output
-added media(3) to author (1) -added author(1) to media (3) ... -added media(5) to author (5) -added author(5) to media (5) Hibernate: insert into ORMREL_AUTHOR_MEDIA (LINK_AUTHOR_ID, LINK_MEDIA_ID) values (?, ?) ... Hibernate: insert into ORMREL_AUTHOR_MEDIA (LINK_AUTHOR_ID, LINK_MEDIA_ID) values (?, ?)
Table of Contents
At the completion of this topic, the student shall
have an understanding of:
Inheritance strategies
Single Table
Table per Concrete Class
Joined
Mapped Superclass for non-entity parent classes
be able to:
Be able to map a Java inheritance relationship using a Single Table strategy
Be able to map a Java inheritance relationship using a Table per Concrete Class strategy
Be able to map a Java inheritance relationship using a Table per Subclass (Join) strategy
Advantages
Simplest to implement
Single table to administer
Performs better than other inheritance strategies
No complex joins
Disadvantages
Unused fields when sub-types have unique properties
Sub-type columns must be nullable
Harder to enforce constraints within database
SQL "check" constraint can help
check(TYPE != 'BREAD_TYPE' or (BAKEDON is not null and SLICES is not null)) check(TYPE != 'Soup' or (SOUPTYPE is not null and EXPIRATION is not null)
Not normalized
More suitable for hierarchies with sub-types that...
Differ primarily in behavior only
Do not have unique data requirements
Figure 63.2. Single Table Example Database Schema
create table ORMINH_PRODUCT ( PTYPE varchar(32) not null, id bigint generated by default as identity, cost double not null, expiration date, SOUPTYPE varchar(16), bakedOn date, slices integer, primary key (id) )
Single table
No joins
Unused columns
Figure 63.3. Single Table Example Java Mapping (Parent Class)
@Entity @Table(name="ORMINH_PRODUCT")
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="PTYPE", //column in root table indicating type
discriminatorType=DiscriminatorType.STRING,//data type of column
length=32) //length of discriminator string
public abstract class Product {
@Id @GeneratedValue
private long id;
private double cost;
...
@Transient
public abstract String getName();
Parent defines default mapping for all derived types
Figure 63.4. Single Table Example Java Mapping (Annotated Derived)
@Entity
@DiscriminatorValue("BREAD_TYPE") //value placed in root table to indicate type
public class Bread extends Product {
private int slices;
@Temporal(TemporalType.DATE)
private Date bakedOn;
...
@Transient
public String getName() { return "Bread"; }
Supplies type-specific column value
Figure 63.5. Single Table Example Java Mapping (Default Derived)
@Entity
public class Soup extends Product {
public enum SoupType {
UNKNOWN("Unknown"),
CHICKEN_NOODLE("Chicken Noodle"),
NEW_ENGLAND_CLAM_CHOWDER("New England Clam Chowder"),
TOMATO("Tomato");
private String text;
private SoupType(String text) { this.text = text; }
public String text() { return text; }
};
@Enumerated(EnumType.STRING)
@Column(name="SOUPTYPE", length=16)
private SoupType type = SoupType.UNKNOWN;
@Temporal(TemporalType.DATE)
private Date expiration;
...
@Transient
public String getName() { return type.text() + "Soup"; }
Accepts default type-specific column value
Figure 63.6. Single Table Example Usage (Persist)
ejava.examples.orm.inheritance.annotated.Soup soup = new Soup();
soup.setCost(2.12);
final long lifetime = 365L*24*60*60*1000;
soup.setExpiration(new Date(System.currentTimeMillis() + lifetime));
soup.setSoupType(Soup.SoupType.CHICKEN_NOODLE);
em.persist(soup);
ejava.examples.orm.inheritance.annotated.Bread bread = new Bread();
bread.setBakedOn(new Date());
bread.setCost(2.25);
bread.setSlices(24);
em.persist(bread);
Hibernate: insert into ORMINH_PRODUCT (id, cost, expiration, SOUPTYPE, PTYPE) values (null, ?, ?, ?, 'Soup') Hibernate: insert into ORMINH_PRODUCT(id, cost, bakedOn, slices, PTYPE) values (null, ?, ?, ?, 'BREAD_TYPE')
Two rows inserted into single base table with discriminator type supplied
Figure 63.7. Single Table Example Usage (Get Entities)
List<Product> products = em.createQuery(
"select p from Product p", Product.class)
.getResultList();
assertTrue("unexpected number of products:" + products.size(),
products.size() == 2);
for(Product p: products) {
log.info("product found:" + p);
}
Hibernate: select product0_.id as id2_9_, product0_.cost as cost3_9_, product0_.expiration as expirati4_9_, product0_.SOUPTYPE as SOUPTYPE5_9_, product0_.bakedOn as bakedOn6_9_, product0_.slices as slices7_9_, product0_.PTYPE as PTYPE1_9_ from ORMINH_PRODUCT product0_ -product found:Soup, id=1, cost=2.12, type=CHICKEN_NOODLE, expiration=2014-10-05 -product found:Bread, id=2, cost=2.25, slices=24, baked=2013-10-05
Single table queried for objects
Figure 63.8. Single Table Example Usage (Verify DB Schema)
//query specific tables for columns
int rows = em.createNativeQuery(
"select ID, TYPE, COST, SOUPTYPE, EXPIRATION, BAKEDON, SLICES " +
" from ORMINH_PRODUCT")
.getResultList().size();
assertEquals("unexpected number of product rows:" + rows, 2, rows);
select * from ORMINH_PRODUCT PTYPE ID COST BAKEDON SLICES EXPIRATION SOUPTYPE ---------- -- ---- ---------- ------ ---------- -------------- Soup 1 2.12 (null) (null) 2007-10-08 CHICKEN_NOODLE BREAD_TYPE 2 2.25 2006-10-08 24 (null) (null)
Single table "sparsely" filled based on type
@DiscriminatorColumn defines column to hold type-specific value
name (default="DTYPE")
Column name
discriminatorType
STRING (defaults to Entity.name)
Only portable technique when accepting default
CHAR
Vendor-specific value when accepting default
INTEGER
Vendor-specific value when accepting default
columnDefinition
Database-specific definition for when generating schema
length
Size of STRING
Advantages
May have constrained columns
No joins when accessing a single concrete type
Disadvantages
Not normalized
Redundant columns in each concrete child table
More work required to query across tables
Requires use of SQL "UNION"
Least desirable from a performance/portability standpoint
More suitable for ...
Sub-types not needed to be manipulated with sibling other sub-types
Figure 64.2. Table per Concrete Class Example Database Schema
create table ORMINH_CHECKING ( id bigint not null, balance double not null, fee double not null, primary key (id) ) create table ORMINH_INTERESTACCT ( id bigint not null, balance double not null, rate double not null, primary key (id) ) create sequence ORMINH_SEQ
Table for each concrete class
No separate table for parent class
Parent columns repeated in concrete sub-class tables
* his particular example uses SEQUENCE for primary key generation
Figure 64.3. Table per Concrete Class Example Java Mapping (Parent Class)
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
@SequenceGenerator(
name="orminhSeq", //required logical name
sequenceName="ORMINH_SEQ" //name in database
)
public abstract class Account {
@Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="orminhSeq")
private long id;
private double balance;
...
public void deposit(double amount) throws AccountException {
setBalance(getBalance() + amount);
}
public abstract void withdraw(double amount) throws AccountException;
...
Parent defines default mapping for all derived types, including PK generation
Using a common SEQUENCE allows sub-classes to have unique PKs across all tables
Figure 64.4. Table per Concrete Class Example Java Mapping (Subclasses)
@Entity
@Table(name="ORMINH_CHECKING")
public class CheckingAccount extends Account {
private double fee;
public void withdraw(double amount) throws AccountException {
super.setBalance(super.getBalance() - fee);
}
@Entity
@Table(name="ORMINH_INTERESTACCT")
public class InterestAccount extends Account {
private double rate;
public void withdraw(double amount) throws AccountException {
super.setBalance(super.getBalance() - amount);
}
Subclasses name their specific entity class table
Figure 64.5. Table per Concrete Class Example Usage (Persist)
ejava.examples.orm.inheritance.annotated.CheckingAccount checking = new CheckingAccount();
checking.setFee(0.50);
em.persist(checking);
ejava.examples.orm.inheritance.annotated.InterestAccount savings = new InterestAccount();
savings.setRate(0.25);
em.persist(savings);
Hibernate: call next value for ORMINH_SEQ Hibernate: insert into ORMINH_CHECKING (balance, fee, id) values (?, ?, ?) Hibernate: insert into ORMINH_INTERESTACCT (balance, rate, id) values (?, ?, ?)
Rows for entities placed into separate tables
Figure 64.6. Table per Concrete Class Example Usage (Get Entities)
List<Account> accounts =em.createQuery("select a from Account a").getResultList();
assertTrue("unexpected number of accounts:" + accounts.size(), accounts.size() == 2);
for(Account a: accounts) {
log.info("account found:" + a);
}
select account0_.id as id1_0_, account0_.balance as balance2_0_, account0_.rate as rate1_7_, account0_.fee as fee1_2_, account0_.clazz_ as clazz_ from ( select id, balance, rate, null as fee, 1 as clazz_ from ORMINH_INTERESTACCT union all select id, balance, null as rate, fee, 2 as clazz_ from ORMINH_CHECKING ) account0_ -account found:InterestAccount, id=51, balance=0.0, rate=0.25 -account found:CheckingAccount, id=50, balance=0.0, fee=0.5
Query through parent type causes SQL "UNION ALL" of each concrete sub-class table
Figure 64.7. Table per Concrete Class Example Usage (Verify DB Schema)
//query specific tables for columns
int rows = em.createNativeQuery(
"select ID, BALANCE, FEE from ORMINH_CHECKING")
.getResultList().size();
assertEquals("unexpected number of checking rows:" + rows, 1, rows);
rows = em.createNativeQuery(
"select ID, BALANCE, RATE from ORMINH_INTERESTACCT")
.getResultList().size();
assertEquals("unexpected number of interestacct rows:" + rows, 1, rows);
select * from ORMINH_CHECKING ID BALANCE FEE -- ------- --- 50 0.0 0.5 select * from ORMINH_INTERESTACCT ID BALANCE RATE -- ------- ---- 51 0.0 0.25
Table per concrete class
No unused columns
Parent columns repeated in each sub-class table
Advantages
Can be normalized
Permits constraints to be defined
Disadvantages
Requires access to multiple tables when using (insert, select, update, and delete) an entity
More suitable for sub-classes that...
Have many unique properties
Require database constraints
Are queried for across sibling sub-types
Figure 65.2. Join Example Database Schema
create table ORMINH_PERSON ( id bigint generated by default as identity, firstName varchar(255), lastName varchar(255), primary key (id) ) create table ORMINH_EMPLOYEE ( hireDate date, payrate double not null, id bigint not null, primary key (id) ) alter table ORMINH_CUSTOMER add constraint FK6D5464A42122B7AC foreign key (id) references ORMINH_PERSON alter table ORMINH_EMPLOYEE add constraint FK9055CB742122B7AC foreign key (id) references ORMINH_PERSON
Table for each entity class is created
Each table has a primary key
Sub-class tables use a primary key join with parent class
Figure 65.3. Join Example Java Mapping (Parent Class)
@Entity @Table(name="ORMINH_PERSON")
@Inheritance(strategy=InheritanceType.JOINED)
public class Person {
@Id @GeneratedValue
private long id;
private String firstName;
private String lastName;
Parent defines default mapping for all derived types
Parent entity class table also defined
Figure 65.4. Join Example Java Mapping (Subclasses)
@Entity
@Table(name="ORMINH_CUSTOMER") //joined with Person table to form Customer
public class Customer extends Person {
public enum Rating { GOLD, SILVER, BRONZE }
@Enumerated(EnumType.STRING)
private Rating rating;
@Entity
@Table(name="ORMINH_EMPLOYEE") //joined with Person table to form Employee
public class Employee extends Person {
private double payrate;
@Temporal(TemporalType.DATE)
private Date hireDate;
Sub-classes define their entity-specific tables
Figure 65.5. Join Example Usage
ejava.examples.orm.inheritance.annotated.Employee employee = new Employee();
employee.setFirstName("john");
employee.setLastName("doe");
employee.setHireDate(new Date());
employee.setPayrate(10.00);
em.persist(employee);
ejava.examples.orm.inheritance.annotated.Customer customer = new Customer();
customer.setFirstName("jane");
customer.setLastName("johnson");
customer.setRating(Customer.Rating.SILVER);
em.persist(customer);
Hibernate: insert into ORMINH_PERSON (id, firstName, lastName) values (null, ?, ?) Hibernate: insert into ORMINH_EMPLOYEE (hireDate, payrate, id) values (?, ?, ?) Hibernate: insert into ORMINH_PERSON (id, firstName, lastName) values (null, ?, ?) Hibernate: insert into ORMINH_CUSTOMER (rating, id) values (?, ?)
Each persist() must insert into concrete class table and parent class table(s)
Figure 65.6. Join Example Usage (Get Entities)
List<Person> people = em.createQuery("select p from Person p", Person.class).getResultList();
assertTrue("unexpected number of people:" + people.size(),
people.size() == 2);
for(Person p: people) {
log.info("person found:" + p);
}
select person0_.id as id1_8_, person0_.firstName as firstNam2_8_, person0_.lastName as lastName3_8_, person0_1_.hireDate as hireDate1_6_, person0_1_.payrate as payrate2_6_, person0_2_.rating as rating1_5_, case when person0_1_.id is not null then 1 when person0_2_.id is not null then 2 when person0_.id is not null then 0 end as clazz_ from ORMINH_PERSON person0_ left outer join ORMINH_EMPLOYEE person0_1_ on person0_.id=person0_1_.id left outer join ORMINH_CUSTOMER person0_2_ on person0_.id=person0_2_.id -person found:Employee, id=1, firstName=john, lastName=doe, payrate=10.0 -person found:Customer, id=2, firstName=jane, lastName=johnson, rating=SILVER
Parent class table joined with each sub-class table during query of parent type
Figure 65.7. Join Example Usage (Verify DB Schema)
int rows = em.createNativeQuery(
"select ID, FIRSTNAME, LASTNAME from ORMINH_PERSON")
.getResultList().size();
assertEquals("unexpected number of person rows:" + rows, 2, rows);
rows = em.createNativeQuery(
"select ID, RATING from ORMINH_CUSTOMER")
.getResultList().size();
assertEquals("unexpected number of customer rows:" + rows, 1, rows);
rows = em.createNativeQuery(
"select ID, PAYRATE, HIREDATE from ORMINH_EMPLOYEE")
.getResultList().size();
assertEquals("unexpected number of employee rows:" + rows, 1, rows);
select * from ORMINH_PERSON ID FIRSTNAME LASTNAME -- --------- -------- 1 john doe 2 jane johnson select * from ORMINH_EMPLOYEE ID HIREDATE PAYRATE -- ---------- ------- 1 2006-10-08 10.0 select * from ORMINH_CUSTOMER ID RATING -- ------ 2 SILVER
Entities span multiple tables
Advantages
Allows inheritance of non-entity classes
Disadvantages
No base entity to form queries across hierarchy (unlike TABLE_PER_CLASS)
Tables not normalized (like TABLE_PER_CLASS)
Parent columns repeated in each subclass table
More suitable for ...
Independent subclasses
Figure 66.2. Non-Entity Example Database Schema
create table ORMINH_ALBUM ( ALBUM_ID bigint generated by default as identity, ALBUM_VERSION bigint, artist varchar(255), title varchar(255), primary key (ALBUM_ID) ) create table ORMINH_TOOTHPASTE ( id bigint generated by default as identity, version bigint not null, size integer not null, primary key (id) )
Non-entity base class properties appear in subclass tables
Figure 66.3. Non-Entity Example Java Mapping (Parent Class)
@MappedSuperclass
public abstract class BaseObject {
private long id;
@Access(AccessType.FIELD)
private long version;
@Transient
public long getId() { return id; }
protected void setId(long id) {
this.id = id;
}
Parent class is not a legal entity -- has no @Id
In this example, the implementation of BaseObject actually has an id attribute that the derived classes make use of. However, it is marked as @Transient in the base class and @Id in the derived Entity classes since MappedSuperClasses do not have primary keys. This specific example could have also used TABLE_PER_CLASS because of the availability of an id property in the base class.
Figure 66.4. Non-Entity Example Java Mapping (Override Defaults)
@Entity
@Table(name="ORMINH_ALBUM") //this table holds both this entity and parent class
@AttributeOverrides({
@AttributeOverride(name="version", column=@Column(name="ALBUM_VERSION"))
})
public class Album extends BaseObject {
@Access(AccessType.FIELD)
private String artist;
@Access(AccessType.FIELD)
private String title;
@Id @GeneratedValue //id is being generated independent of other siblings
@Column(name="ALBUM_ID")
public long getId() { return super.getId(); }
protected void setId(long id) {
super.setId(id);
}
"version" column name from parent being renamed
"id" property defined in this class given custom column name
Transient attribute in parent being reused to hold @Id for subclass using PROPERTY access
Figure 66.5. Non-Entity Example Java Mapping (Default Derived)
@Entity
@Table(name="ORMINH_TOOTHPASTE") //table holds this entity and parent class
public class ToothPaste extends BaseObject {
@Access(AccessType.FIELD)
private int size;
@Id @GeneratedValue //id is being generated independent of other siblings
public long getId() { return super.getId(); }
protected void setId(long id) {
super.setId(id);
}
Entity accepts mapping defaults
Figure 66.6. Non-Entity Example Usage
ejava.examples.orm.inheritance.annotated.Album album = new Album();
album.setArtist("Lynyrd Skynyrd");
album.setTitle("One More for the Road");
em.persist(album);
ejava.examples.orm.inheritance.annotated.ToothPaste toothpaste = new ToothPaste();
toothpaste.setSize(10);
em.persist(toothpaste);
Hibernate: insert into ORMINH_ALBUM (ALBUM_ID, ALBUM_VERSION, artist, title) values (null, ?, ?, ?) Hibernate: insert into ORMINH_TOOTHPASTE (id, version, size) values (null, ?, ?)
Rows are inserted into type-specific entity class tables
Figure 66.7. Non-Entity Example Usage (Get Entities)
List<BaseObject> objects = em.createQuery("select a from Album a").getResultList();
objects.addAll( em.createQuery("select tp from ToothPaste tp").getResultList());
assertTrue("unexpected number of objects:" + objects.size(), objects.size() == 2);
for(BaseObject o: objects) {
log.info("object found:" + o);
}
Hibernate: select album0_.ALBUM_ID as ALBUM1_1_, album0_.ALBUM_VERSION as ALBUM2_1_, album0_.artist as artist3_1_, album0_.title as title4_1_ from ORMINH_ALBUM album0_ Hibernate: select toothpaste0_.id as id1_12_, toothpaste0_.version as version2_12_, toothpaste0_.size as size3_12_ from ORMINH_TOOTHPASTE toothpaste0_ -object found:ejava.examples.orm.inheritance.annotated.Album@3822f407, id=1, name=Lynyrd Skynyrd:One More for the Road -object found:ejava.examples.orm.inheritance.annotated.ToothPaste@22a79bc, id=1, name=10oz toothpaste
Separate tables are accessed when obtaining each type
Figure 66.8. Non-Entity Example Usage (Verify DB Schema)
int rows = em.createNativeQuery(
"select ALBUM_ID, ALBUM_VERSION, ARTIST, TITLE " +
" from ORMINH_ALBUM")
.getResultList().size();
assertEquals("unexpected number of album rows:" + rows, 1, rows);
rows = em.createNativeQuery(
"select ID, VERSION, SIZE " +
" from ORMINH_TOOTHPASTE")
.getResultList().size();
assertEquals("unexpected number of toothpaste rows:" + rows, 1, rows);
select * from ORMINH_ALBUM ALBUM_ID ALBUM_VERSION ARTIST TITLE -------- ------------- -------------- --------------------- 1 0 Lynyrd Skynyrd One More for the Road select * from ORMINHTOOTHPASTE ID VERSION SIZE -- ------- ---- 1 0 10
Separate tables per concrete class (like TABLE_PER_CLASS)
No unused columns (like TABLE_PER_CLASS)
Figure 67.2. Generated Database Schema
create table ORMINH_SHAPE ( id bigint generated by default as identity, version bigint not null, posx integer not null, posy integer not null, primary key (id) ) create table ORMINH_CIRCLE ( radius integer not null, id bigint not null, primary key (id) ) create table ORMINH_RECTANGLE ( height integer not null, width integer not null, id bigint not null, primary key (id) ) alter table ORMINH_CIRCLE add constraint FKFF2F1F1632C97600 foreign key (id) references ORMINH_SHAPE alter table ORMINH_RECTANGLE add constraint FK1FFF614932C97600 foreign key (id) references ORMINH_SHAPE
create table ORMINH_CUBE ( depth integer not null, id bigint not null, primary key (id) ) alter table ORMINH_CUBE add constraint FK84203FB112391CE foreign key (id) references ORMINH_RECTANGLE
Provider used parent's JOIN strategy over child's TABLE_PER_CLASS specification
Table of Contents
Provide breadth coverage of JPA Queries to demonstrate options available for accessing information from a relational database using an EntityManager.
At the completion of this topic, the student shall
have an understanding of:
Query Construction
Value, ResultClass, and Entity Queries
Typed Queries
Dynamic and Named Queries
Single, Multiple, and Stream Results
Parameters
Paging
Locking
Supported Query Languages
JPA Query Language
Native SQL
Java-based Criteria API
be able to:
Form a query using...
JPA-QL
Native SQL
Criteria API
Use a query to locate specific properties for one or more entities that match a criteria
Use a query to locate specific entities that match a criteria
Use paging within queries to handle large data sets
Use pessimistic locking to better support database consistency
Table of Contents
Three fundamental query types within JPA
JPA Query Language (JPA) - entity/property/relationship-based
Native SQL - table/column-based
Criteria API - entity/property/relationship-based using Java classes
Access to the *entity* model using a SQL-like text query language
Queries expressed using entities, properties, and relationships
Pros
More concise (than other query forms)
Familiar to SQL users
Abstracts query away from table, column, primary key, and relationship mapping
Can be defined within XML deployment descriptors
Produces portable SQL
Cons
Not (overly) type-safe
No help from Java compiler in constructing query expression
Don't find out most errors until runtime
Figure 68.1. Building a JPA Query using JPA-QL
String jpaqlString =
"select c from Customer c " +
"where c.firstName = :firstName " +
"order by c.lastName ASC";
//use query string to build typed JPA-QL query
TypedQuery<Customer> query = em
.createQuery(jpaqlString,Customer.class);
"c" is part of root query
"c" represents rows from Customer entity table(s)
"c.firstName and c.lastName" are property paths off root term
":firstName" is parameter placeholder
"Customer.class" type parameter allows for a type-safe return result
Figure 68.2. Executing a JPA Query (built from JPA-QL)
//at this point we are query-type agnostic
List<Customer> customers = query
.setParameter("firstName", "thing")
.getResultList();
logger.info("result={}" + customers);
assertEquals("unexpected number of results", 2, customers.size());
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=? order by customer0_.LAST_NAME ASC -result=[firstName=thing, lastName=one, firstName=thing, lastName=two]
Placeholder is replaced by runtime parameter
Zero-or-more results are requested
Entities returned are managed
Figure 68.3. Condensing the JPA-QL Query
List<Customer> customers = em.createQuery(
"select c from Customer c " +
"where c.firstName = :firstName " +
"order by c.lastName ASC",
Customer.class)
.setParameter("firstName", "thing")
.getResultList();
logger.info("result={}" + customers);
assertEquals("unexpected number of results", 2, customers.size());
Access to power of working with native SQL
Pros
Provides full access to native SQL power
Provides full access to database-vendor SQL extensions
Easy to see when native SQL is being used within application -- target for portability review
Ability to produce managed entity as result of query
Easy use ad-hoc of SQL UNIONs across tables
Cons
Portability of SQL not addressed by JPA
Not type-safe
No help from Java compiler in constructing query expression
Don't find out most errors until runtime
Figure 68.4. Building a JPA Query using Native SQL
Table table = Customer.class.getAnnotation(Table.class);
String sqlString =
"select c.CUSTOMER_ID, c.FIRST_NAME, c.LAST_NAME " +
String.format("from %s c ", table.name()) +
"where c.FIRST_NAME = ? " +
"order by c.LAST_NAME ASC";
//use query string to build query
Query query = em.createNativeQuery(sqlString,Customer.class);
"c" represents rows in table
specific columns (or *) are return for each row
"?" marks a positional parameter -- non-portable to use named parameters in native SQL queries
TypedQuery<T>s not supported in native SQL queries because of a conflict with legacy JPA 1.0 API
Figure 68.5. Executing a JPA Query (built from Native SQL)
//at this point we are query-type agnostic (mostly)
@SuppressWarnings("unchecked")
List<Customer> customers = query
.setParameter(1, "thing")
.getResultList();
log.info("result=" + customers);
assertEquals("unexpected number of results", 2, customers.size());
select c.CUSTOMER_ID, c.FIRST_NAME, c.LAST_NAME from JPAQL_CUSTOMER c where c.FIRST_NAME = ? order by c.LAST_NAME ASC -result=[firstName=thing, lastName=one, firstName=thing, lastName=two]
Query execution similar to other query types
User-provided SQL executed
Legacy JPA 1.0 Native SQL query syntax already used the signature of passing in a Class for createNativeQuery(). In this context, it was an entity class that contained JPA mappings for the query -- not the returned entity type. This prevented createNativeQuery() from being updated to return a typed result in JPA 2.0.
Figure 68.6. Condensing the SQL Query
@SuppressWarnings("unchecked")
List<Customer> customers = em.createNativeQuery(
"select c.CUSTOMER_ID, c.FIRST_NAME, c.LAST_NAME " +
"from JPAQL_CUSTOMER c " +
"where c.FIRST_NAME = ? " +
"order by c.LAST_NAME ASC",
Customer.class)
.setParameter(1, "thing")
.getResultList();
logger.info("result={}" + customers);
assertEquals("unexpected number of results", 2, customers.size());
Allow query to return mixture of managed entities and values
DAOs can use value results to plugin transient aggregate properties in parent entity without pulling entire child entities back from database
e.g., total sales for clerk
Figure 68.7. NativeQuery with SqlResultSetMapping
@SuppressWarnings("unchecked")
List<Object[]> results = em.createNativeQuery(
"select clerk.CLERK_ID, "
+ "clerk.FIRST_NAME, "
+ "clerk.LAST_NAME, "
+ "clerk.HIRE_DATE, "
+ "clerk.TERM_DATE, "
+ "sum(sales.amount) total_sales "
+ "from JPAQL_CLERK clerk "
+ "left outer join JPAQL_SALE_CLERK_LINK slink on clerk.CLERK_ID=slink.CLERK_ID "
+ "left outer join JPAQL_SALE sales on sales.SALE_ID=slink.SALE_ID "
+ "group by clerk.CLERK_ID, "
+ "clerk.FIRST_NAME, "
+ "clerk.LAST_NAME, "
+ "clerk.HIRE_DATE, "
+ "clerk.TERM_DATE "
+ "order by total_sales DESC",
"Clerk.clerkSalesResult")
.getResultList();
@Entity @Table(name="JPAQL_CLERK")
@SqlResultSetMappings({
@SqlResultSetMapping(name = "Clerk.clerkSalesResult",
entities={ @EntityResult(entityClass = Clerk.class )},
columns={@ColumnResult(name = "total_sales")}
)
})
public class Clerk {
Figure 68.8. Example NativeQuery with SqlResultSetMapping Output
for (Object[] result: results) {
Clerk clerk = (Clerk) result[0];
BigDecimal totalSales = (BigDecimal) result[1];
log.info(String.format("%s, $ %s", clerk.getFirstName(), totalSales));
}
-Manny, $ 250.00 -Moe, $ 150.00 -Jack, $ null
Figure 68.9. NamedNativeQuery with SqlResultSetMapping
@Entity @Table(name="JPAQL_CLERK")
@NamedNativeQueries({
@NamedNativeQuery(name = "Clerk.clerkSales", query =
"select clerk.CLERK_ID, "
+ "clerk.FIRST_NAME, "
+ "clerk.LAST_NAME, "
+ "clerk.HIRE_DATE, "
+ "clerk.TERM_DATE, "
+ "sum(sales.amount) total_sales "
+ "from JPAQL_CLERK clerk "
+ "left outer join JPAQL_SALE_CLERK_LINK slink on clerk.CLERK_ID=slink.CLERK_ID "
+ "left outer join JPAQL_SALE sales on sales.SALE_ID=slink.SALE_ID "
+ "group by clerk.CLERK_ID, "
+ "clerk.FIRST_NAME, "
+ "clerk.LAST_NAME, "
+ "clerk.HIRE_DATE, "
+ "clerk.TERM_DATE "
+ "order by total_sales DESC",
resultSetMapping="Clerk.clerkSalesResult")
})
@SqlResultSetMappings({
@SqlResultSetMapping(name = "Clerk.clerkSalesResult",
entities={ @EntityResult(entityClass = Clerk.class )},
columns={@ColumnResult(name = "total_sales")}
)
})
public class Clerk {
Figure 68.10. Example NamedNativeQuery with SqlResultSetMapping Usage
List<Object[]> results = em.createNamedQuery("Clerk.clerkSales").getResultList();
Somewhat parallel capability to JPAQL
Build overall query using Java types (demonstrated here with "string accessors")
Pros
Structure of query is type-safe
Allows object-level manipulation of the query versus manipulation of a query string
Useful when building query structure based on runtime properties
Cons
Complex -- looses familiarity with SQL
Cannot be expressed in XML deployment descriptor
Access to properties not type-safe (addressed by Canonical Metamodel)
Figure 68.11. Building a JPA Query using Criteria API
select c from Customer c where c.firstName = :firstName order by c.lastName ASC
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c)
.where(cb.equal(c.get("firstName"),
cb.parameter(String.class,"firstName")))
.orderBy(cb.asc(c.get("lastName")));
//build query from criteria definition
TypedQuery<Customer> query = em.createQuery(qdef);
"CriteriaBuilder" used as starting point to build objects within the query tree
"CriteriaQuery<T>" used to hold the definition of query
"Root<T>" used to reference root level query terms
"CriteriaBuilder.from()" used to designate the entity that represents root query term
Result used to create path references for query body
"CriteriaBuilder.select()" officially lists the objects returned from query
"CriteriaBuilder.where()" builds a decision predicate of which entities to include
"CriteriaBuilder.equal()" builds an equals predicate for the where clause
"Root<T>.get()" returns the property referenced in path expression
"CriteriaBuilder.parameter()" builds a parameter placeholder within query. Useful with @Temporal date comparisons
Figure 68.12. Executing a JPA Query using Criteria API
//at this point we are query-type agnostic
List<Customer> customers = query
.setParameter("firstName", "thing")
.getResultList();
logger.info("result={}" + customers);
assertEquals("unexpected number of results", 2, customers.size());
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=? order by customer0_.LAST_NAME asc -result=[firstName=thing, lastName=one, firstName=thing, lastName=two]]
Query execution identical to JPA-QL case
Figure 68.13. Condensing the Criteria API Query
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Customer> c = qdef.from(Customer.class);
List<Customer> customers = em.createQuery(qdef.select(c)
.where(cb.equal(c.get("firstName"), "thing"))
.orderBy(cb.asc(c.get("lastName"))))
.getResultList();
logger.info("result={}" + customers);
assertEquals("unexpected number of results", 2, customers.size());
Previous Criteria API examples were string label based -- not type safe
Criteria API provides means for stronger typing
Strong typing permits automatic detection of model and query differences
Provides access to the persistent model backing each entity and its properties
Figure 68.14. Accessing JPA Metamodel
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Customer> c = qdef.from(Customer.class);
EntityType<Customer> c_ = c.getModel();
logger.info(String.format("%7s, %10s:%-30s",
c_.getPersistenceType(),
c_.getName(),
c_.getJavaType()));
for (Attribute<? super Customer, ?> p: c_.getAttributes()) {
logger.info(String.format("%7s, %10s:%-30s",
p.getPersistentAttributeType(),
p.getName(),
p.getJavaType()));
}
- ENTITY, Customer:class ejava.jpa.examples.query.Customer - BASIC, firstName:class java.lang.String - BASIC, id:long - BASIC, lastName:class java.lang.String
JPA Metamodel provides access to
Entity structure
Entity database mapping
Pros
Access properties in (a more) type-safe manner
Cons
Complex
No compiler warning of entity type re-factoring
Figure 68.15. Building Query with JPA Metamodel
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Customer> c = qdef.from(Customer.class);
EntityType<Customer> c_ = c.getModel();
qdef.select(c) //we are returning a single root object
.where(cb.equal(
c.get(c_.getSingularAttribute("firstName", String.class)),
cb.parameter(String.class,"firstName")))
.orderBy(cb.asc(c.get(c_.getSingularAttribute("lastName", String.class))));
TypedQuery<Customer> query = em.createQuery(qdef);
Access to properties within entities done through type-safe accessors
Figure 68.16. Executing Query with JPA Metamodel
//at this point we are query-type agnostic
List<Customer> customers = query
.setParameter("firstName", "thing")
.getResultList();
logger.info("result={}" + customers);
assertEquals("unexpected number of results", 2, customers.size());
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=? order by customer0_.LAST_NAME asc -result=[firstName=thing, lastName=one, firstName=thing, lastName=two]
Results identical to previous approaches
Figure 68.17. Condensing the JPA Metamodel-based Query
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Customer> c = qdef.from(Customer.class);
EntityType<Customer> c_ = c.getModel();
List<Customer> customers = em.createQuery(qdef.select(c)
.where(cb.equal(
c.get(c_.getSingularAttribute("firstName", String.class)), "thing"))
.orderBy(cb.asc(c.get(c_.getSingularAttribute("lastName", String.class)))))
.getResultList();
logger.info("result={}" + customers);
assertEquals("unexpected number of results", 2, customers.size());
Complexities of metamodel can be simplified using metamodel classes
Pros
Easy, type-safe access to entity model
Java compiler can alert of mismatch between query and entity model
Cons
Requires either manual construct or auto-generation of separate metamodel class
Figure 68.18. Example Canonical Metamodel
package ejava.jpa.examples.query;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;
@StaticMetamodel(Customer.class)
public abstract class Customer_ {
public static volatile SingularAttribute<Customer, Long> id;
public static volatile SingularAttribute<Customer, String> lastName;
public static volatile SingularAttribute<Customer, String> firstName;
}
Construct or generate a canonical metamodel class to provide type-safe, easy access to properties
Figure 68.19. Building Query with Canonical Metamodel
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c) //we are returning a single root object
.where(cb.equal(
c.get(Customer_.firstName),
cb.parameter(String.class,"firstName")))
.orderBy(cb.asc(c.get(Customer_.lastName)));
TypedQuery<Customer> query = em.createQuery(qdef);
Use canonical metamodel class to provide type-safe, easy access to properties ("Customer_.firstName")
Figure 68.20. Executing Query with Canonical Metamodel
//at this point we are query-type agnostic
List<Customer> customers = query
.setParameter("firstName", "thing")
.getResultList();
logger.info("result={}" + customers);
assertEquals("unexpected number of results", 2, customers.size());
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=? order by customer0_.LAST_NAME asc -result=[firstName=thing, lastName=one, firstName=thing, lastName=two]
Result is identical to previous approaches
Figure 68.21. Condensing the Canonical Metamodel-based Query
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Customer> c = qdef.from(Customer.class);
List<Customer> customers = em.createQuery(qdef.select(c)
.where(cb.equal(c.get(Customer_.firstName),"thing"))
.orderBy(cb.asc(c.get(Customer_.lastName))))
.getResultList();
logger.info("result={}" + customers);
assertEquals("unexpected number of results", 2, customers.size());
More work to get here but clean, result
Type-safe - queries will not compile if entity changes
Canonical Metamodel classes can be manually authoried or generated
Figure 68.22. Maven Dependency Can Generate Canonical Metamodel Classes
<!-- generates JPA metadata classes -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate-entitymanager.version}</version>
<scope>provided</scope>
</dependency>
Figure 68.23. Generated Source placed in target/generated-sources/annotations
`-- target |-- generated-sources `-- annotations `-- ejava `-- jpa `-- examples `-- query |-- Clerk_.java |-- Customer_.java |-- Sale_.java `-- Store_.java
Figure 68.24. Maven Plugin adds Generated Source to IDE Build Path
<!-- add generated JPA metamodel classes to classpath -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.9.1</version>
<executions>
<execution>
<id>add-metamodel-classes</id>
<phase>process-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>target/generated-sources/annotations</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
Three basic forms for query expression
SqlResultSetMapping
JPAQL
Native SQL
Criteria API
String-based Accessors
Metamodel Accessors
Canonical Metamodel Accessors
Entity model provides portability
JPAQL
Criteria API
Native SQL provides direct access to
full power of SQL
full access to database-specific extensions
Criteria API provides type-safe construct of query structure
JPA Metamodel provides type-safe access to entity properties
JPA Canonical Metamodel provides type-safe access to model-specific entity properties
Produces compilation error when query our of sync with entity model
Provides convenient access to model-specific properties
Create query using JPA-QL String
javax.persistence.Query createQuery(String jpaql);
<T extends Object> javax.persistence.TypedQuery<T> createQuery(String jpaql, Class<T>);
Create query using native SQL
javax.persistence.Query createNativeQuery(String sql);
javax.persistence.Query createNativeQuery(String sql, Class sqlMapping);
javax.persistence.Query createNativeQuery(String sql, String sqlMapping);
Create query using Criteria API
javax.persistence.criteria.CriteriaBuilder getCriteriaBuilder();
javax.persistence.metamodel.Metamodel getMetamodel();
<T extends Object> javax.persistence.TypedQuery<T> createQuery(javax.persistence.criteria.CriteriaQuery<T>);
Create query from Named Query
javax.persistence.Query createNamedQuery(String queryName);
<T extends java.lang.Object> javax.persistence.TypedQuery<T> createNamedQuery(String queryName, Class<T>);
Obtains exactly one result
TypedQuery returns type-safe result
Figure 69.1. Get a Unique Object based on Query
TypedQuery<Store> query = em.createQuery(
"select s from Store s where s.name='Big Al''s'", Store.class);
Store store = query.getSingleResult();
select store0_.STORE_ID as STORE1_4_, store0_.name as name2_4_ from ORMQL_STORE store0_ where store0_.name='Big Al''s'
Figure 69.2. Throws NoResultException when not Found
try {
store = em.createQuery(
"select s from Store s where s.name='A1 Sales'", Store.class)
.getSingleResult();
}
catch (NoResultException ex) { ... }
Figure 69.3. Throws NonUniqueResultException when multiple Found
try {
Clerk clerk = em.createQuery(
"select c from Clerk c where lastName='Pep'", Clerk.class)
.getSingleResult();
}
catch (NonUniqueResultException ex) { ... }
Returns zero or more results
TypedQuery returns type-safe result
Figure 69.4. Return List of Results Based on Query
TypedQuery<Clerk> query = em.createQuery(
"select c from Clerk c where lastName='Pep'", Clerk.class);
List<Clerk> clerks = query.getResultList();
assertTrue("unexpected number of clerks:" + clerks.size(), clerks.size() > 1);
for(Clerk c : clerks) {
logger.info("found clerk: {}", c);
}
select clerk0_.CLERK_ID as CLERK1_0_, clerk0_.FIRST_NAME as FIRST2_0_, clerk0_.HIRE_DATE as HIRE3_0_, clerk0_.LAST_NAME as LAST4_0_, clerk0_.TERM_DATE as TERM5_0_ from JPAQL_CLERK clerk0_ where clerk0_.LAST_NAME='Pep' ... -found clerk:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, } -found clerk:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, } -found clerk:firstName=Jack, lastName=Pep, hireDate=1973-03-01, termDate=null, sales(0)={}
Returns a java.util.stream.Stream
Useful when obtaining large results set, bypass paging
Allows query results to be scrolled by a processing stream
Figure 69.5. Return Stream of Results based on Query
TypedQuery<Customer> query = em.createQuery(
"select c from Customer as c",
Customer.class);
Stream<Customer> s = query.getResultStream();
Map<String, Customer> resultMap = s.collect(
Collectors.toMap(c->c.getFirstName()+c.getLastName(), c->c ));
for (Entry<String, Customer> result : resultMap.entrySet()) {
logger.info("found={}", result);
}
int rows = resultMap.size();
assertTrue("unexpected number of customers", rows > 0);
select customer0_.CUSTOMER_ID as CUSTOMER1_1_, customer0_.FIRST_NAME as FIRST_NA2_1_, customer0_.LAST_NAME as LAST_NAM3_1_ from JPAQL_CUSTOMER customer0_
The java.util.stream
capability contains many features
that perform filtering of results -- emulating the purpose of the SQL where
clause. The database where clause is a much better place to implement filters.
Avoid using streams in an attempt to override core RDBMS capability when
the data is available within the database.
Runtime query parameters passed into query
Figure 69.6. Name-based Query Parameters
TypedQuery<Customer> query = em.createQuery(
"select c from Customer c " +
"where c.firstName=:firstName and c.lastName=:lastName",
Customer.class);
query.setParameter("firstName", "cat");
query.setParameter("lastName", "inhat");
Customer customer = query.getSingleResult();
assertNotNull(customer);
logger.info("found customer for param names: {}", customer);
select customer0_.CUSTOMER_ID as CUSTOMER1_1_, customer0_.FIRST_NAME as FIRST2_1_, customer0_.LAST_NAME as LAST3_1_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=? and customer0_.LAST_NAME=? -found customer for param names:firstName=cat, lastName=inhat
:firstName and :lastName act as placeholders for runtime query parameters
Runtime parameters supplied using placeholder names
A parameter for each placeholder must be supplied - no defaults
A placeholder must exist for each parameter supplied - no extras
Figure 69.7. Ordinal-based Parameters
query = em.createQuery(
"select c from Customer c " +
"where c.firstName=?1 and c.lastName like ?2", Customer.class);
query.setParameter(1, "thing");
query.setParameter(2, "%");
List<Customer> customers = query.getResultList();
assertTrue("unexpected number of customers:" + customers.size(),
customers.size() == 2);
for(Customer c : customers) {
logger.info("found customer for param position: {}", c);
}
select customer0_.CUSTOMER_ID as CUSTOMER1_1_, customer0_.FIRST_NAME as FIRST2_1_, customer0_.LAST_NAME as LAST3_1_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=? and ( customer0_.LAST_NAME like ? ) -found customer for param position:firstName=thing, lastName=one -found customer for param position:firstName=thing, lastName=two
Appended numbers (?1) assign an ordinal value
No numbers supplied (?) cause default value based on order
Figure 69.8. Date-based Parameters
Calendar hireDate = Calendar.getInstance();
hireDate.set(Calendar.YEAR, 1972);
TypedQuery<Clerk> query = em.createQuery(
"select c from Clerk c " +
"where c.hireDate > :date", Clerk.class);
query.setParameter("date", hireDate.getTime(), TemporalType.DATE);
Clerk clerk = query.getSingleResult();
logger.info("found clerk by date({}): {}", hireDate.getTime(), clerk);
select clerk0_.CLERK_ID as CLERK1_0_, clerk0_.FIRST_NAME as FIRST2_0_, clerk0_.HIRE_DATE as HIRE3_0_, clerk0_.LAST_NAME as LAST4_0_, clerk0_.TERM_DATE as TERM5_0_ from JPAQL_CLERK clerk0_ where clerk0_.HIRE_DATE>? ... -found clerk by date(Fri Oct 06 20:28:08 EDT 1972):firstName=Jack, lastName=Pep, hireDate=1973-03-01, termDate=null, sales(0)={}
Dates are specified as DATE, TIME, or TIMESTAMP
Figure 69.9.
TypedQuery<Sale> query = em.createQuery(
"select s from Sale s", Sale.class);
for(int i=0; i<2; i++) {
List<Sale> sales = query.setMaxResults(10)
.setFirstResult(i)
.getResultList();
for(Sale s: sales) {
logger.info("found sale in page({}): {}", i, s);
em.detach(s); //we are done with this
}
}
select sale0_.SALE_ID as SALE1_2_, sale0_.amount as amount2_2_, sale0_.BUYER_ID as BUYER3_2_, sale0_.date as date4_2_, sale0_.SALE_STORE as SALE5_2_ from JPAQL_SALE sale0_ limit ? ... -found sale in page(0):date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, } -found sale in page(0):date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, } select sale0_.SALE_ID as SALE1_2_, sale0_.amount as amount2_2_, sale0_.BUYER_ID as BUYER3_2_, sale0_.date as date4_2_, sale0_.SALE_STORE as SALE5_2_ from JPAQL_SALE sale0_ limit ? offset ? ... -found sale in page(1):date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Offset and limits passed to database
Database provides specified subset of rows
Obtain a locked copy of entity -- ready for modification
Required for some concurrent interactions with database
Figure 69.10. Obtaining a Pessimistic Write Lock
//get a list of clerks to update -- locked so others cannot change
List<Clerk> clerks = em.createQuery(
"select c from Clerk c " +
"where c.hireDate > :date", Clerk.class)
.setParameter("date", new GregorianCalendar(1972,Calendar.JANUARY,1).getTime())
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.setHint("javax.persistence.lock.timeout", 0)
.getResultList();
//make changes
for (Clerk c: clerks) {
c.setHireDate(new GregorianCalendar(1972, Calendar.FEBRUARY, 1).getTime());
}
select clerk0_.CLERK_ID as CLERK1_0_, clerk0_.FIRST_NAME as FIRST2_0_, clerk0_.HIRE_DATE as HIRE3_0_, clerk0_.LAST_NAME as LAST4_0_, clerk0_.TERM_DATE as TERM5_0_ from JPAQL_CLERK clerk0_ where clerk0_.HIRE_DATE>? for update ...
Provider adds database-specific technique for lock
Lock timeout (in msecs) can be expressed through query hint
Not all databases support lock timeouts
Change database -- not query it
Bypasses cache -- cached entities out of sync with database changes
Criteria API updates/deletes added in JPA 2.1
Figure 69.11. JPA-QL Bulk Update Example
Query update = em.createQuery(
"update Clerk c set c.lastName=:newlast where c.lastName=:last");
update.setParameter("last", "Pep");
update.setParameter("newlast", "Peppy");
int rows = update.executeUpdate();
assertEquals("unexpected rows updated:" + rows, clerks.size(), rows);
update JPAQL_CLERK set LAST_NAME=? where LAST_NAME=?
Change directly applied to database, not the cached entity
Number of entities changed returned
Figure 69.12. Criteria API Bulk Update Example
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaUpdate<Clerk> qdef2=cb.createCriteriaUpdate(Clerk.class);
//"update Clerk c set c.lastName=:newlast where c.lastName=:last"
Root<Clerk> c2 = qdef2.from(Clerk.class);
qdef2.set("lastName", "Peppy")
.where(cb.equal(c2.get("lastName"), "Pep"));
Query update = em.createQuery(qdef2);
int rows = update.executeUpdate();
assertEquals("unexpected rows updated:" + rows, clerks.size(), rows);
Figure 69.13. JPA-QL Bulk Delete Example
Query update = em.createQuery(
"delete from Customer c " +
"where c.firstName like :first AND c.lastName like :last");
int rows = update.setParameter("first", "thing")
.setParameter("last", "%")
.executeUpdate();
assertTrue("no rows updated", rows > 0);
delete from JPAQL_CUSTOMER where ( FIRST_NAME like ? ) and ( LAST_NAME like ? )
Bulk deletes do not trigger cascades
Entity instance exists in memory even after deleted from database
Figure 69.14. Criteria API Bulk Update Example
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaDelete<Customer> delete = cb.createCriteriaDelete(Customer.class);
//"delete from Customer c " +
//"where c.firstName like :first AND c.lastName like :last");
Root<Customer> c2 = delete.from(Customer.class);
delete.where(cb.and(
cb.like(c.<String>get("firstName"), "thing"),
cb.like(c.<String>get("lastName"), "%")
));
Query update = em.createQuery(delete);
int rows = update.executeUpdate();
assertTrue("no rows updated", rows > 0);
Figure 69.15. Refresh/Clear/Detach Stale Entit(ies)
//re-sync entity with DB changes
em.refresh(clerk);
//evict all managed entities in persistence context
em.clear();
//remove entity from persistence context
em.detach(clerk);
Keeping stale entities around will produce confusing results
"em.clear()" should be avoided except at end of transaction since un-manages everything
Register query with provider rather than ad-hoc
Available for JPA-QL and Native SQL -- not available with Criteria API
Can be registered using class annotations and orm.xml descriptor
LockMode and hints can be specified in declaration
Figure 69.16. Named Query Annotations Applied to (any) Entity Class
@Entity
@Table(name="JPAQL_CUSTOMER")
@NamedQueries({
@NamedQuery(name="Customer.getCustomersByName",
query="select c from Customer c " +
"where c.firstName like :first AND c.lastName like :last"),
@NamedQuery(name="Customer.getCustomerPurchases",
query="select s from Sale s " +
"where s.buyerId=:custId")
})
public class Customer {
Figure 69.17. Using Named Query
Customer customer =
em.createNamedQuery("Customer.getCustomersByName", Customer.class)
.setParameter("first", "cat")
.setParameter("last", "inhat")
.getResultList()
.get(0);
assertNotNull("no customer found", customer);
Figure 69.18. Named Native Query Annotation Example
@Entity
@Table(name="JPAQL_CUSTOMER")
@NamedNativeQueries({
@NamedNativeQuery(name="Customer.getCustomerRows",
query="select * from JPAQL_CUSTOMER c " +
"where c.FIRST_NAME = ?1")
})
public class Customer {
Example query uses Native SQL to return all columns for table
Figure 69.19. Using Named Native Query
@SuppressWarnings("unchecked")
List<Object[]> rows = em.createNamedQuery("Customer.getCustomerRows")
.setParameter(1, "cat")
.getResultList();
assertEquals("unexpected customers found", 1, rows.size());
logger.info("found customer: {}", Arrays.toString(rows.get(0)));
select * from JPAQL_CUSTOMER c where c.FIRST_NAME = ? -found customer:[1, cat, inhat]
Table of Contents
"select" defines root query objects -- all path references must start from this set
"from" defines source of root query terms
"as" (optional) identifies a variable assignment of entity in from clause
"object()" (optional) identifies what is returned for the path expressed in select clause (e.g., object(), count()) -- left over from EJBQL
no "where" clause indicates all entities are selected
Figure 70.3. Using a JPA-QL Query
TypedQuery<Customer> query = em.createQuery(
"select object(c) from Customer as c",
Customer.class);
List<Customer> results = query.getResultList();
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ -found result:firstName=cat, lastName=inhat -found result:firstName=thing, lastName=one -found result:firstName=thing, lastName=two
Allows return of simple property
"c.lastName" is called a "path"
All paths based from root query terms
Single path selects return typed list of values
Figure 70.5. Using Non-Entity Query
TypedQuery<String> query = em.createQuery(
"select c.lastName from Customer c", String.class);
List<String> results = query.getResultList();
select customer0_.LAST_NAME as col_0_0_ from JPAQL_CUSTOMER customer0_ -lastName=inhat -lastName=one -lastName=two
Query result is a List<String> because "c.lastName" is a String
Select specifies multiple terms
Terms are expressed thru a path expression
Terms must be based off paths from root terms in the FROM (or JOIN) clause
Figure 70.7. Using Object[] Multi-select Query
TypedQuery<Object[]> query = em.createQuery(
"select c.firstName, c.hireDate from Clerk c", Object[].class);
List<Object[]> results = query.getResultList();
assertTrue("no results", results.size() > 0);
for(Object[] result : results) {
assertEquals("unexpected result length", 2, result.length);
String firstName = (String) result[0];
Date hireDate = (Date) result[1];
log.info("firstName=" + firstName + " hireDate=" + hireDate);
}
select clerk0_.FIRST_NAME as col_0_0_, clerk0_.HIRE_DATE as col_1_0_ from JPAQL_CLERK clerk0_ -firstName=Manny hireDate=1970-01-01 -firstName=Moe hireDate=1970-01-01 -firstName=Jack hireDate=1973-03-01
Query defined to return elements of select in Object[]
Figure 70.8. Multi-select Query with Tuple Example
select c.firstName as firstName, c.hireDate as hireDate from Clerk c
Aliases may be assigned to select terms for named-access to results
Figure 70.9. Using Tuple Multi-select Query
TypedQuery<Tuple> query = em.createQuery(
"select c.firstName as firstName, c.hireDate as hireDate from Clerk c", Tuple.class);
List<Tuple> results = query.getResultList();
assertTrue("no results", results.size() > 0);
for(Tuple result : results) {
assertEquals("unexpected result length", 2, result.getElements().size());
String firstName = result.get("firstName", String.class);
Date hireDate = result.get("hireDate", Date.class);
log.info("firstName=" + firstName + " hireDate=" + hireDate);
}
select clerk0_.FIRST_NAME as col_0_0_, clerk0_.HIRE_DATE as col_1_0_ from JPAQL_CLERK clerk0_ -firstName=Manny hireDate=1970-01-01 -firstName=Moe hireDate=1970-01-01 -firstName=Jack hireDate=1973-03-01
Query defined to return instances of Tuple class
Tuples provide access using
get(index) - simular to Object[]
get(index, Class<T> resultType) - typed access by index
get(alias) - access by alias
get(alias, Class<T> resultType) - typed access by alias
getElements() - access thru collection interface
Figure 70.10. Multi-select Query with Constructor Example
select new ejava.jpa.examples.query.Receipt(s.id, s.buyerId, s.date, s.amount) from Sale s
Individual elements of select are matched up against class constructor
Figure 70.11. Example ResultClass
package ejava.jpa.examples.query;
...
public class Receipt {
private long saleId;
private long customerId;
private Date date;
private double amount;
public Receipt(long saleId, long customerId, Date date, BigDecimal amount) {
this(customerId, saleId, date, amount.doubleValue());
}
public Receipt(long saleId, long customerId, Date date, double amount) {
this.customerId = customerId;
this.saleId = saleId;
this.date = date;
this.amount = amount;
}
...
Constructed class may be simple POJO -- no need to be an entity
Instances are not managed
Suitable for use as Data Transfer Objects (DTOs)
Figure 70.12. Using Constructor Multi-select Query
TypedQuery<Receipt> query = em.createQuery(
String.format("select new %s(", Receipt.class.getName()) +
"s.id,s.buyerId,s.date, s.amount) " +
"from Sale s", Receipt.class);
List<Receipt> results = query.getResultList();
for(Receipt receipt : results) {
assertNotNull("no receipt", receipt);
log.info("receipt=" + receipt);
}
select sale0_.SALE_ID as col_0_0_, sale0_.BUYER_ID as col_1_0_, sale0_.date as col_2_0_, sale0_.amount as col_3_0_ from JPAQL_SALE sale0_ -receipt=sale=1, customer=1, date=1998-04-10 10:13:35, amount=$100.00 -receipt=sale=2, customer=2, date=1999-06-11 14:15:10, amount=$150.00
Each row returned as instance of provided class
All paths based off root-level FROM (or JOIN) terms
Paths use dot (".") notation to change contexts
Paths -- used this way -- must always express a single element. Must use JOINs for paths involving collections
Paths that cross entity boundaries automatically add a join to SQL query
Figure 70.14. Using Single Element Path Expression
TypedQuery<Object[]> query = em.createQuery(
"select s.id, s.store.name from Sale s", Object[].class);
List<Object[]> results = query.getResultList();
assertTrue("no results", results.size() > 0);
for(Object[] result : results) {
assertEquals("unexpected result length", 2, result.length);
Long id = (Long) result[0];
String name = (String) result[1];
log.info("sale.id=" + id + ", sale.store.name=" + name);
}
select sale0_.SALE_ID as col_0_0_, store1_.name as col_1_0_ from JPAQL_SALE sale0_, ORMQL_STORE store1_ where sale0_.SALE_STORE=store1_.STORE_ID -sale.id=1, sale.store.name=Big Al's -sale.id=2, sale.store.name=Big Al's
Automatic INNER JOIN formed between Sale and Store because of the cross-entity path
Cannot directly navigate a XxxToMany relationship without a join
Figure 70.16. Correct Collection Path Expression
select sale.date from Clerk c INNER JOIN c.sales sale
Collection ("sales") is brought in as a root term ("sale") of the query through a JOIN expression
JOINs will match entities by their defined primary/foreign keys
INNER JOIN will return only those entities where there is a match
INNER JOIN is the default
Figure 70.19. Collection Path Expression SQL Output
select sale2_.date as col_0_0_ from JPAQL_CLERK clerk0_ inner join JPAQL_SALE_CLERK_LINK sales1_ on clerk0_.CLERK_ID=sales1_.CLERK_ID inner join JPAQL_SALE sale2_ on sales1_.SALE_ID=sale2_.SALE_ID -found result:1998-04-10 10:13:35.0 -found result:1999-06-11 14:15:10.0 -found result:1999-06-11 14:15:10.0
(Many-to-Many) Link table used during JOIN
Tables automatically joined on primary keys
Only Sales sold by our Clerks are returned
Figure 70.20. LEFT OUTER JOIN Example
select c.id, c.firstName, sale.amount from Clerk c LEFT OUTER JOIN c.sales sale
LEFT is the default for OUTER JOIN
Figure 70.21. Alternate LEFT OUTER JOIN Form
select c.id, c.firstName, sale.amount from Clerk c LEFT JOIN c.sales sale
LEFT OUTER JOIN will return root with or without related entities
Figure 70.22. LEFT OUTER JOIN Runtime SQL Output
select clerk0_.CLERK_ID as col_0_0_, clerk0_.FIRST_NAME as col_1_0_, sale2_.amount as col_2_0_ from JPAQL_CLERK clerk0_ left outer join JPAQL_SALE_CLERK_LINK sales1_ on clerk0_.CLERK_ID=sales1_.CLERK_ID left outer join JPAQL_SALE sale2_ on sales1_.SALE_ID=sale2_.SALE_ID -clerk.id=1, clerk.firstName=Manny, amount=100.00 -clerk.id=1, clerk.firstName=Manny, amount=150.00 -clerk.id=2, clerk.firstName=Moe, amount=150.00 -clerk.id=3, clerk.firstName=Jack, amount=null
(Many-to-Many) Link table used during JOIN
Tables automatically joined on primary keys
All clerks, with or without a Sale, are returned
Figure 70.23. Explicit Collection Path Example
select c from Sale s, Customer c where c.id = s.buyerId
Permits JOINs without relationship in entity model
Figure 70.24. Explicit Collection Path SQL Output
select customer1_.CUSTOMER_ID as CUSTOMER1_3_, customer1_.FIRST_NAME as FIRST2_3_, customer1_.LAST_NAME as LAST3_3_ from JPAQL_SALE sale0_ cross join JPAQL_CUSTOMER customer1_ where customer1_.CUSTOMER_ID=sale0_.BUYER_ID -found result:firstName=cat, lastName=inhat -found result:firstName=thing, lastName=one
Returns all Customers that are identified by a Sale
Figure 70.25. Example Query Resulting in Lazy Fetch
select s from Store s JOIN s.sales where s.name='Big Al''s'
A normal JOIN (implicit or explicit) may honor the fetch=LAZY property setting of the relation
Can be exactly what is desired
Can also cause problems or extra work if not desired
Figure 70.26. Example Entity with Lazy Fetch Declared for Relation
@Entity @Table(name="ORMQL_STORE")
public class Store {
...
@OneToMany(mappedBy="store",
cascade={CascadeType.REMOVE},
fetch=FetchType.LAZY)
private List<Sale> sales = new ArrayList<Sale>();
Sales are lazily fetched when obtaining Store
Figure 70.27. Example Lazy Fetch Problem
Store store = em2.createQuery(
"select s from Store s JOIN s.sales " +
"where s.name='Big Al''s'",
Store.class).getSingleResult();
em2.close();
try {
store.getSales().get(0).getAmount();
fail("did not trigger lazy initialization exception");
} catch (LazyInitializationException expected) {
log.info("caught expected exception:" + expected);
}
select store0_.STORE_ID as STORE1_0_, store0_.name as name0_ from ORMQL_STORE store0_ inner join JPAQL_SALE sales1_ on store0_.STORE_ID=sales1_.SALE_STORE where store0_.name='Big Al''s' limit ? -caught expected exception:org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: ejava.jpa.examples.query.Store.sales, no session or session was closed
Accessing the Sale properties causes a LazyInitializationException when persistence context no longer active or accessible
Note that only a single row is required to be returned from the database for a fetch=LAZY relation. Although it requires more queries to the database, it eliminates duplicate parent information for each child row and can eliminate the follow-on query all together when not accessed.
Figure 70.28. Example Eager Fetch Query
select s from Store s JOIN FETCH s.sales where s.name='Big Al''s'
A JOIN FETCH used to eager load related entities as side-effect of query
Can be used as substitute for fetch=EAGER specification on relation
Figure 70.29. Example Eager Fetch SQL Output
select store0_.STORE_ID as STORE1_0_0_, sales1_.SALE_ID as SALE1_1_1_, store0_.name as name0_0_, sales1_.amount as amount1_1_, sales1_.BUYER_ID as BUYER3_1_1_, sales1_.date as date1_1_, sales1_.SALE_STORE as SALE5_1_1_, sales1_.SALE_STORE as SALE5_0_0__, sales1_.SALE_ID as SALE1_0__ from ORMQL_STORE store0_ inner join JPAQL_SALE sales1_ on store0_.STORE_ID=sales1_.SALE_STORE where store0_.name='Big Al''s'
Sales are eagerly fetched when obtaining Store
Note that adding JOIN FETCH to parent query causes the parent rows to be repeated for each eagerly loaded child row and eliminated by the provider. This requires fewer database queries but results in more (and redundant) data to be returned from the query.
Limits output to unique value combinations
Figure 70.31. Distinct Example Output
select distinct customer0_.LAST_NAME as col_0_0_ from JPAQL_CUSTOMER customer0_ -found result:two -found result:inhat -found result:one
Found three unique last names
Figure 70.33. Another Distinct Example Output
select distinct customer0_.FIRST_NAME as col_0_0_ from JPAQL_CUSTOMER customer0_ -found result:cat -found result:thing
Found two unique first names
Figure 71.1. Example Equality Test
select c from Customer c where c.firstName='cat'
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME='cat' -found result:firstName=cat, lastName=inhat
Return entities where there is an equality match
Figure 71.2. Escaping Special Characters
select s from Store s where s.name='Big Al''s'
select store0_.STORE_ID as STORE1_0_, store0_.name as name0_ from ORMQL_STORE store0_ where store0_.name='Big Al''s' ... -found result:name=Big Al's, sales(2)={1, 2, }
Escaped special character is passed through to the database
Figure 71.3. Like Test Literal
select c from Clerk c where c.firstName like 'M%'
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where clerk0_.FIRST_NAME like 'M%' ... -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, } -found result:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }
Figure 71.5. Using Like Test Literal Parameter
TypedQuery<T> query = em.createQuery(ejbqlString, resultType);
query.setParameter("firstName", "M%");
List<T> objects = query.getResultList();
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where clerk0_.FIRST_NAME like ? -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, } -found result:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }
Figure 71.6. Like Test Concatenated String
select c from Clerk c where c.firstName like concat(:firstName,'%')
Figure 71.7. Using Like Test Concatenated String
TypedQuery<T> query = em.createQuery(ejbqlString, resultType);
query.setParameter("firstName", "M");
List<T> objects = query.getResultList();
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where clerk0_.FIRST_NAME like (?||'%') -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, } -found result:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }
Figure 71.8. Like Test Single Character Wildcard
select c from Clerk c where c.firstName like '_anny'
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where clerk0_.FIRST_NAME like '_anny' -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, }
Figure 71.10. Using Formula
String jpaql = "select count(s) from Sale s " +
"where (s.amount * :tax) > :amount";
TypedQuery<Number> query = em.createQuery(jpaql, Number.class)
.setParameter("amount", new BigDecimal(10.00));
//keep raising taxes until somebody pays $10.00 in tax
double tax = 0.05;
for (;query.setParameter("tax", new BigDecimal(tax))
.getSingleResult().intValue()==0;
tax += 0.01) {
log.debug("tax=" + NumberFormat.getPercentInstance().format(tax));
}
log.info("raise taxes to: " + NumberFormat.getPercentInstance().format(tax));
select count(sale0_.SALE_ID) as col_0_0_ from JPAQL_SALE sale0_ where sale0_.amount*?>? limit ? -tax=5% select count(sale0_.SALE_ID) as col_0_0_ from JPAQL_SALE sale0_ where sale0_.amount*?>? limit ? -tax=6% select count(sale0_.SALE_ID) as col_0_0_ from JPAQL_SALE sale0_ where sale0_.amount*?>? limit ? -raise taxes to: 7%
Figure 71.11. Logic Operator Example
select c from Customer c where (c.firstName='cat' AND c.lastName='inhat') OR c.firstName='thing'
Figure 71.12. Logic Operator Example Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME='cat' and customer0_.LAST_NAME='inhat' or customer0_.FIRST_NAME='thing' -found result:firstName=cat, lastName=inhat -found result:firstName=thing, lastName=one -found result:firstName=thing, lastName=two
Figure 71.13. Another Logic Operator Example
select c from Customer c where (NOT (c.firstName='cat' AND c.lastName='inhat')) OR c.firstName='thing'
Figure 71.14. Another Logic Operator Example Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME<>'cat' or customer0_.LAST_NAME<>'inhat' or customer0_.FIRST_NAME='thing' -found result:firstName=thing, lastName=one -found result:firstName=thing, lastName=two
Must compare values
Of same type
Of legal promotion type
Can compare 123:int to 123:long
Cannot compare 123:int to "123":string
Can compare entities
Compare entities and not primary/foreign key values
Figure 71.16. Using Entity Equality Query
//get a clerk entity
Clerk clerk = em.createQuery(
"select c from Clerk c where c.firstName = 'Manny'",
Clerk.class)
.getSingleResult();
//find all sales that involve this clerk
List<Sale> sales = em.createQuery(
"select s from Sale s " +
"JOIN s.clerks c " +
"where c = :clerk",
Sale.class)
.setParameter("clerk", clerk)
.getResultList();
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where clerk0_.FIRST_NAME='Manny' limit ?
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ inner join JPAQL_SALE_CLERK_LINK clerks1_ on sale0_.SALE_ID=clerks1_.SALE_ID inner join JPAQL_CLERK clerk2_ on clerks1_.CLERK_ID=clerk2_.CLERK_ID where clerk2_.CLERK_ID=? ... -found=date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, } ... -found=date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Figure 71.17. Example Between Query
select s from Sale s where s.amount BETWEEN :low AND :high
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.amount between ? and ? ... -found result:date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, }
Figure 71.18. Another Example Between Query
select s from Sale s where s.amount NOT BETWEEN :low AND :high
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.amount not between ? and ? ... -found result:date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Can be used to test for unassigned value or relationship
Figure 71.19. Example Test for Null
select s from Sale s where s.store IS NULL
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.SALE_STORE is null
Figure 71.20. Example Test for Not Null
select s from Sale s where s.store IS NOT NULL
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.SALE_STORE is not null ... -found result:date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, } ... -found result:date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Can be used to test for an empty collection
Figure 71.21. Example Empty Collection Test
select c from Clerk c where c.sales IS EMPTY
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where not (exists ( select sale2_.SALE_ID from JPAQL_SALE_CLERK_LINK sales1_, JPAQL_SALE sale2_ where clerk0_.CLERK_ID=sales1_.CLERK_ID and sales1_.SALE_ID=sale2_.SALE_ID)) ... -found result:firstName=Jack, lastName=Pep, hireDate=1973-03-01, termDate=null, sales(0)={}
Sub-select returns values from collection under test
Outer query tests for no existing (EMPTY)values
Figure 71.22. Example Non-Empty Test
select c from Clerk c where c.sales IS NOT EMPTY
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where exists ( select sale2_.SALE_ID from JPAQL_SALE_CLERK_LINK sales1_, JPAQL_SALE sale2_ where clerk0_.CLERK_ID=sales1_.CLERK_ID and sales1_.SALE_ID=sale2_.SALE_ID ) ... -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, } ... -found result:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }
Sub-select returns values from collection under test
Outer query tests for existing (NOT EMPTY)values
Can be used to determine membership in a collection
Figure 71.23. Example Membership Test
select c from Clerk c where c.firstName = 'Manny'
select s from Sale s where :clerk MEMBER OF s.clerks
Defines a shorthand for a subquery
Figure 71.24. Using Membership Test
//get a clerk entity
Clerk clerk = em.createQuery(
"select c from Clerk c where c.firstName = 'Manny'",
Clerk.class)
.getSingleResult();
//find all sales that involve this clerk
List<Sale> sales = em.createQuery(
"select s from Sale s " +
"where :clerk MEMBER OF s.clerks",
Sale.class)
.setParameter("clerk", clerk)
.getResultList();
Figure 71.25. Using Membership Test Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where clerk0_.FIRST_NAME='Manny' limit ?
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where ? in ( select clerk2_.CLERK_ID from JPAQL_SALE_CLERK_LINK clerks1_, JPAQL_CLERK clerk2_ where sale0_.SALE_ID=clerks1_.SALE_ID and clerks1_.CLERK_ID=clerk2_.CLERK_ID ) ... -found=date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, } ... -found=date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Useful when query cannot be expressed through JOINs
Figure 71.26. Example Subquery
select c from Customer c where c.id IN (select s.buyerId from Sale s where s.amount > 100)
Figure 71.27. Example Subquery Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.CUSTOMER_ID in ( select sale1_.BUYER_ID from JPAQL_SALE sale1_ where sale1_.amount>100 ) -found result:firstName=thing, lastName=one
All existing values must meet criteria (i.e., no value may fail criteria)
Zero values is the lack of failure (i.e., meets criteria)
Figure 71.28. Example ALL Query
select c from Clerk c where 125 < ALL (select s.amount from c.sales s)
List all clerks that have all sales above $125.00 or none at all
-or- List all clerks with no sale <= $125.00
Figure 71.29. Example ALL Query Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where 125<all ( select sale2_.amount from JPAQL_SALE_CLERK_LINK sales1_, JPAQL_SALE sale2_ where clerk0_.CLERK_ID=sales1_.CLERK_ID and sales1_.SALE_ID=sale2_.SALE_ID ) ... -found result:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, } ... -found result:firstName=Jack, lastName=Pep, hireDate=1973-03-01, termDate=null, sales(0)={}
Manny excluded because has 1 sale below $125.00
Moe included because has only $150.00 sale
Jack included because has no sales that fail criteria
Figure 71.30. Another ALL Query Example
select c from Clerk c where 125 > ALL (select s.amount from c.sales s)
List all clerks that have all sales below $125.00 or none at all
-or- List all clerks with no sale >= $125.00
Figure 71.31. Another ALL Query Example Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where 125>all ( select sale2_.amount from JPAQL_SALE_CLERK_LINK sales1_, JPAQL_SALE sale2_ where clerk0_.CLERK_ID=sales1_.CLERK_ID and sales1_.SALE_ID=sale2_.SALE_ID ) -found result:firstName=Jack, lastName=Pep, hireDate=1973-03-01, termDate=null, sales(0)={}
Manny excluded because has 1 sale above $125.00
Moe excluded because has only $150.00 sale
Jack included because has no sales that fail criteria
Any matching value meets criteria (i.e., one match and you are in)
Zero values fails to meet the criteria (i.e., must have at least one matching value)
Figure 71.32. Example ANY Query
select c from Clerk c where 125 < ANY (select s.amount from c.sales s)
List all clerks that have at least one sale above $125.00
Figure 71.33. Example ANY Query Runtime Output
-executing query:select c from Clerk c where 125 < ANY (select s.amount from c.sales s) select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where 125<any ( select sale2_.amount from JPAQL_SALE_CLERK_LINK sales1_, JPAQL_SALE sale2_ where clerk0_.CLERK_ID=sales1_.CLERK_ID and sales1_.SALE_ID=sale2_.SALE_ID ) ... -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, } ... -found result:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }
Manny included because has 1 sale above $125.00
Moe included because $150.00 sale qualifies him as well
Jack excluded because has no sales that meet criteria
Figure 71.34. Another Example ANY Query
select c from Clerk c where 125 > ANY (select s.amount from c.sales s)
List all clerks that have at least one sale below $125.00
Figure 71.35. Another Example ANY Query Runtime Output
-executing query:select c from Clerk c where 125 > ANY (select s.amount from c.sales s) select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where 125>any ( select sale2_.amount from JPAQL_SALE_CLERK_LINK sales1_, JPAQL_SALE sale2_ where clerk0_.CLERK_ID=sales1_.CLERK_ID and sales1_.SALE_ID=sale2_.SALE_ID ) -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, }
Manny included because has 1 sale below $125.00
Moe excluded because his only $150.00 sale above criteria
Jack excluded because has no sales that meet criteria
Figure 72.1. Example String Compare
select c from Customer c where c.firstName='CAT'
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME='CAT'
No rows found because 'CAT' does not match anything because of case
Figure 72.2. Example LOWER Function
select c from Customer c where c.firstName=LOWER('CAT')
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=lower('CAT') -found result:firstName=cat, lastName=inhat
One customer found because case-sensitive compare now correct
Figure 72.3. Example UPPER Function
select UPPER(c.firstName) from Customer c where c.firstName=LOWER('CAT')
select upper(customer0_.FIRST_NAME) as col_0_0_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=lower('CAT') -found result:CAT
First name of customer located returned in upper case
Figure 72.4. Example TRIM Function
select TRIM(LEADING 'c' FROM c.firstName) from Customer c where c.firstName='cat')
select trim(LEADING 'c' FROM customer0_.FIRST_NAME) as col_0_0_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME='cat' -found result:at
Customer's name, excluding initial 'c' character, returned
Figure 72.5. Example CONCAT Function]
select c from Customer c where CONCAT(CONCAT(c.firstName,' '),c.lastName) ='cat inhat')
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where ( (customer0_.FIRST_NAME||' ')||customer0_.LAST_NAME)='cat inhat' -found result:firstName=cat, lastName=inhat
Customer located after concatenation of fields yields match
Figure 72.6. Example LENGTH Function
select c from Customer c where LENGTH(c.firstName) = 3
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where length(customer0_.FIRST_NAME)=3 -found result:firstName=cat, lastName=inhat
Customer found where length of firstName matches specified length criteria
Figure 72.7. Example LOCATE Function
select c from Customer c where LOCATE('cat',c.firstName,2) > 0
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where locate('cat', customer0_.FIRST_NAME, 2)>0
No firstName found with 'cat' starting at position=2
Figure 72.8. Another Example LOCATE Function
select c from Customer c where LOCATE('at',c.firstName,2) > 1
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where locate('at', customer0_.FIRST_NAME, 2)>1 -found result:firstName=cat, lastName=inhat
firstName found with 'at' starting at a position 2
Figure 72.9. Example SUBSTRING Function
select SUBSTRING(c.firstName,2,2) from Customer c where c.firstName = 'cat'
select substring(customer0_.FIRST_NAME, 2, 2) as col_0_0_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME='cat' -found result:at
Return the two character substring of firstName starting at position two
Figure 72.10. Another Example SUBSTRING Function
select c from Customer c where SUBSTRING(c.firstName,2,2) = 'at'
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where substring(customer0_.FIRST_NAME, 2, 2)='at' -found result:firstName=cat, lastName=inhat
Find the customer with a two characters starting a position two of firstName equaling 'at'
CURRENT_DATE
CURRENT_TIME
CURRENT_TIMESTAMP
Figure 72.11. CURRENT_DATE Query Example
select s from Sale s where s.date < CURRENT_DATE
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.date<CURRENT_DATE ... -found result:date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, } ... -found result:date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Located two Sales that occurred prior to today's date
Figure 72.12. Another CURRENT_DATE Query Example
select s from Sale s where s.date = CURRENT_DATE
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.date=CURRENT_DATE
Located no sales on today's date
Figure 72.13. Using Bulk Update to Change Date
update Sale s set s.date = CURRENT_DATE
update JPAQL_SALE set date=CURRENT_DATE
Update all sales to today
Figure 72.14. Retrying CURRENT_DATE Query After Bulk Update
select s from Sale s where s.date = CURRENT_DATE
-executing query:select s from Sale s where s.date = CURRENT_DATE select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.date=CURRENT_DATE ... -found result:date=2013-06-05 00:00:00, amount=$100.00, buyer=1, clerks(1)={1, } ... -found result:date=2013-06-05 00:00:00, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Now locating sales for today's date
Bulk commands (i.e., update) invalidate cached entities. You must refresh their state with the database or detach/clear them from the persistence context to avoid using out-dated information.
ASC - ascending order
DESC - descending order
Figure 72.15. Example Order By
select s from Sale s ORDER BY s.amount ASC
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ order by sale0_.amount ASC ... -found result:date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, } ... -found result:date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Note the ASC order on amount
Figure 72.16. Another Example Order By
select s from Sale s ORDER BY s.amount DESC
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ order by sale0_.amount DESC -found result:date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, } -found result:date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, }
Note the DESC order on amount
Figure 72.17. Example COUNT Aggregate Function
select COUNT(s) from Sale s
select count(*) as col_0_0_ from JPAQL_SALE sale0_ -found result:2
Figure 72.18. Example MIN Aggregate Function
select min(s.amount) from Sale s
select min(sale0_.amount) as col_0_0_ from JPAQL_SALE sale0_ -found result:100.00
Figure 72.19. Example MAX Aggregate Function
select max(s.amount) from Sale s
select max(sale0_.amount) as col_0_0_ from JPAQL_SALE sale0_ -found result:150.00
Figure 72.20. Example SUM Aggregate Function
select sum(s.amount) from Sale s
select sum(sale0_.amount) as col_0_0_ from JPAQL_SALE sale0_ -found result:250.00
Figure 72.21. Example AVE Aggregate Function
select ave(s.amount) from Sale s
select avg(cast(sale0_.amount as double)) as col_0_0_ from JPAQL_SALE sale0_ -found result:125.0
Get count of sales for each clerk
Figure 72.23. Example Group By Runtime Output
select clerk0_.CLERK_ID as col_0_0_, count(sale2_.SALE_ID) as col_1_0_, clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ left outer join JPAQL_SALE_CLERK_LINK sales1_ on clerk0_.CLERK_ID=sales1_.CLERK_ID left outer join JPAQL_SALE sale2_ on sales1_.SALE_ID=sale2_.SALE_ID group by clerk0_.CLERK_ID ... -found=[firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, }, 2] ... -found=[firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }, 1] ... -found=[firstName=Jack, lastName=Pep, hireDate=1973-03-01, termDate=null, sales(0)={}, 0]
Figure 72.24. Example Having Aggregate Function
select c, COUNT(s) from Clerk c LEFT JOIN c.sales s GROUP BY c HAVING COUNT(S) <= 1
Provide a list of Clerks and their count of Sales for counts <= 1
Figure 72.25. Example Having Aggregate Function Runtime Output
select clerk0_.CLERK_ID as col_0_0_, count(sale2_.SALE_ID) as col_1_0_, clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ left outer join JPAQL_SALE_CLERK_LINK sales1_ on clerk0_.CLERK_ID=sales1_.CLERK_ID left outer join JPAQL_SALE sale2_ on sales1_.SALE_ID=sale2_.SALE_ID group by clerk0_.CLERK_ID having count(sale2_.SALE_ID)<=1 ... -found=[firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }, 1] ... -found=[firstName=Jack, lastName=Pep, hireDate=1973-03-01, termDate=null, sales(0)={}, 0]
Wed matched on Moe (1 sale) and Jack (0 sales)
Table of Contents
Following sections contain but may not show the following code in all cases
Figure 73.1. Boiler-plate code
CriteriaBuilder cb = em.getCriteriaBuilder();
//example-specific criteria API definition goes here
CriteriaQuery<T> qdef = ...
//repeated display loop eliminated
TypedQuery<T> query = em.createQuery(qdef);
List<T> objects = query.getResultList();
for(T o: objects) {
log.info("found result:" + o);
}
Get CriteriaBuilder from EntityManager
Build example-specific query definition in CriteriaQuery using CriteriaBuilder
Execute Query/Print results
Figure 73.3. Criteria API Definition
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c);
"from"
defines source of root query terms
returns object leveraged in query body
"select" defines root query objects -- all path references must start from this set
no "where" clause indicates all entities are selected
Figure 73.4. In Programming Context
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c);
TypedQuery<Customer> query = em.createQuery(qdef);
List<Customer> results = query.getResultList();
Figure 73.5. Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ -found result:firstName=cat, lastName=inhat -found result:firstName=thing, lastName=one -found result:firstName=thing, lastName=two
Figure 73.7. Criteria API Definition
CriteriaQuery<String> qdef = cb.createQuery(String.class);
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c.<String>get("lastName"));
Allows return of simple property
c.get("lastName") is called a "path"
All paths based from root query terms (thus requirement for Root<Customer> c object)
Single path selects return typed list of values
Figure 73.8. In Programming Context
CriteriaQuery<String> qdef = cb.createQuery(String.class);
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c.<String>get("lastName"));
TypedQuery<String> query = em.createQuery(qdef);
List<String> results = query.getResultList();
Query result is a List<String> because "c.lastName" is a String
Figure 73.9. Runtime Output
select customer0_.LAST_NAME as col_0_0_ from JPAQL_CUSTOMER customer0_ -lastName=inhat -lastName=one -lastName=two
Figure 73.11. Criteria API Definition
CriteriaQuery<Object[]> qdef = cb.createQuery(Object[].class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(cb.array(c.get("firstName"), c.get("hireDate")));
Select specifies multiple terms within array()
Terms are expressed thru a path expression
Terms must be based off paths from root terms in the FROM (or JOIN) clause -- thus why Root<Clerk> c was retained from cb.from() call
Figure 73.12. In Programming Context
CriteriaQuery<Object[]> qdef = cb.createQuery(Object[].class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(cb.array(c.get("firstName"), c.get("hireDate")));
TypedQuery<Object[]> query = em.createQuery(qdef);
List<Object[]> results = query.getResultList();
assertTrue("no results", results.size() > 0);
for(Object[] result : results) {
assertEquals("unexpected result length", 2, result.length);
String firstName = (String) result[0];
Date hireDate = (Date) result[1];
log.info("firstName=" + firstName + " hireDate=" + hireDate);
}
Query defined to return elements of select in Object[]
Figure 73.13. Runtime Output
select clerk0_.FIRST_NAME as col_0_0_, clerk0_.HIRE_DATE as col_1_0_ from JPAQL_CLERK clerk0_ -firstName=Manny hireDate=1970-01-01 -firstName=Moe hireDate=1970-01-01 -firstName=Jack hireDate=1973-03-01
Figure 73.15. Criteria API Definition
CriteriaQuery<Tuple> qdef = cb.createTupleQuery();
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(cb.tuple(
c.get("firstName").alias("firstName"),
c.get("hireDate").alias("hireDate")));
Aliases may be assigned to select terms for named-access to results
Figure 73.16. In Programming Context
CriteriaQuery<Tuple> qdef = cb.createTupleQuery();
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(cb.tuple(
c.get("firstName").alias("firstName"),
c.get("hireDate").alias("hireDate")));
TypedQuery<Tuple> query = em.createQuery(qdef);
List<Tuple> results = query.getResultList();
assertTrue("no results", results.size() > 0);
for(Tuple result : results) {
assertEquals("unexpected result length", 2, result.getElements().size());
String firstName = result.get("firstName", String.class);
Date hireDate = result.get("hireDate", Date.class);
log.info("firstName=" + firstName + " hireDate=" + hireDate);
}
Query defined to return instances of Tuple class
Tuples provide access using
get(index) - simular to Object[]
get(index, Class<T> resultType) - typed access by index
get(alias) - access by alias
get(alias, Class<T> resultType) - typed access by alias
getElements() - access thru collection interface
Figure 73.17. Runtime Output
select clerk0_.FIRST_NAME as col_0_0_, clerk0_.HIRE_DATE as col_1_0_ from JPAQL_CLERK clerk0_ -firstName=Manny hireDate=1970-01-01 -firstName=Moe hireDate=1970-01-01 -firstName=Jack hireDate=1973-03-01
Figure 73.18. Equivalent JPAQL
select new ejava.jpa.examples.query.Receipt(s.id,s.buyerId,s.date, s.amount) from Sale s
Figure 73.19. Criteria API Definition
CriteriaQuery<Receipt> qdef = cb.createQuery(Receipt.class);
Root<Sale> s = qdef.from(Sale.class);
qdef.select(cb.construct(
Receipt.class,
s.get("id"),
s.get("buyerId"),
s.get("date"),
s.get("amount")));
Individual elements of select() are matched up against class constructor
Figure 73.20. In Programming Context
CriteriaQuery<Receipt> qdef = cb.createQuery(Receipt.class);
Root<Sale> s = qdef.from(Sale.class);
qdef.select(cb.construct(
Receipt.class,
s.get("id"),
s.get("buyerId"),
s.get("date"),
s.get("amount")));
TypedQuery<Receipt> query = em.createQuery(qdef);
List<Receipt> results = query.getResultList();
assertTrue("no results", results.size() > 0);
for(Receipt receipt : results) {
assertNotNull("no receipt", receipt);
log.info("receipt=" + receipt);
}
Constructed class may be simple POJO -- no need to be an entity
Instances are not managed
Suitable for use as Data Transfer Objects (DTOs)
Figure 73.21. Runtime Output
select sale0_.SALE_ID as col_0_0_, sale0_.BUYER_ID as col_1_0_, sale0_.date as col_2_0_, sale0_.amount as col_3_0_ from JPAQL_SALE sale0_ -receipt=sale=1, customer=1, date=1998-04-10 10:13:35, amount=$100.00 -receipt=sale=2, customer=2, date=1999-06-11 14:15:10, amount=$150.00
Figure 73.23. Criteria API Definition
CriteriaQuery<Object[]> qdef = cb.createQuery(Object[].class);
Root<Sale> s = qdef.from(Sale.class);
qdef.select(cb.array(s.get("id"),
s.get("store").get("name")));
All paths based off root-level FROM (or JOIN) terms
Paths use call chaining to change contexts
Paths -- used this way -- must always express a single element. Must use JOINs for paths involving collections
All paths based off root-level FROM (or JOIN) terms
Figure 73.24. In Programming Context
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> qdef = cb.createQuery(Object[].class);
Root<Sale> s = qdef.from(Sale.class);
qdef.select(cb.array(s.get("id"),
s.get("store").get("name")));
TypedQuery<Object[]> query = em.createQuery(qdef);
List<Object[]> results = query.getResultList();
assertTrue("no results", results.size() > 0);
for(Object[] result : results) {
assertEquals("unexpected result length", 2, result.length);
Long id = (Long) result[0];
String name = (String) result[1];
log.info("sale.id=" + id + ", sale.store.name=" + name);
}
Figure 73.25. Runtime Output
select sale0_.SALE_ID as col_0_0_, store1_.name as col_1_0_ from JPAQL_SALE sale0_, ORMQL_STORE store1_ where sale0_.SALE_STORE=store1_.STORE_ID -sale.id=1, sale.store.name=Big Al's -sale.id=2, sale.store.name=Big Al's
Automatic INNER JOIN formed between Sale and Store because of the cross-entity path
Figure 73.27. Criteria API Definition
CriteriaQuery<Date> qdef = cb.createQuery(Date.class);
Root<Clerk> c = qdef.from(Clerk.class);
Join<Clerk, Sale> sale = c.join("sales", JoinType.INNER);
qdef.select(sale.<Date>get("date"));
Collection is brought in as a root term of the query through a JOIN expression
JOINs will match entities by their defined primary/foreign keys
INNER JOIN will return only those entities where there is a match
INNER JOIN is default JoinType when none specified
Figure 73.28. Runtime Output
select sale2_.date as col_0_0_ from JPAQL_CLERK clerk0_ inner join JPAQL_SALE_CLERK_LINK sales1_ on clerk0_.CLERK_ID=sales1_.CLERK_ID inner join JPAQL_SALE sale2_ on sales1_.SALE_ID=sale2_.SALE_ID -found result:1998-04-10 10:13:35.0 -found result:1999-06-11 14:15:10.0 -found result:1999-06-11 14:15:10.0
(Many-to-Many) Link table used during JOIN
Tables automatically joined on primary keys
Only Sales sold by our Clerks are returned
Figure 73.29. Equivalent JPAQL
select c.id, c.firstName, sale.amount from Clerk c LEFT JOIN c.sales sale
Figure 73.30. Criteria API Definition
CriteriaQuery<Object[]> qdef = cb.createQuery(Object[].class);
Root<Clerk> c = qdef.from(Clerk.class);
Join<Clerk, Sale> sale = c.join("sales", JoinType.LEFT);
qdef.select(cb.array(c.get("id"),
c.get("firstName"),
sale.get("amount")));
LEFT OUTER JOIN will return root with or without related entities
Figure 73.31. Runtime Output
select clerk0_.CLERK_ID as col_0_0_, clerk0_.FIRST_NAME as col_1_0_, sale2_.amount as col_2_0_ from JPAQL_CLERK clerk0_ left outer join JPAQL_SALE_CLERK_LINK sales1_ on clerk0_.CLERK_ID=sales1_.CLERK_ID left outer join JPAQL_SALE sale2_ on sales1_.SALE_ID=sale2_.SALE_ID -clerk.id=1, clerk.firstName=Manny, amount=100.00 -clerk.id=1, clerk.firstName=Manny, amount=150.00 -clerk.id=2, clerk.firstName=Moe, amount=150.00 -clerk.id=3, clerk.firstName=Jack, amount=null
(Many-to-Many) Link table used during JOIN
Tables automatically joined on primary keys
All clerks, with or without a Sale, are returned
Figure 73.33. Criteria API Definition
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Sale> s = qdef.from(Sale.class);
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c)
.where(cb.equal(c.get("id"), s.get("buyerId")));
Permits JOINs without relationship in entity model
Figure 73.34. Runtime Output
select customer1_.CUSTOMER_ID as CUSTOMER1_3_, customer1_.FIRST_NAME as FIRST2_3_, customer1_.LAST_NAME as LAST3_3_ from JPAQL_SALE sale0_ cross join JPAQL_CUSTOMER customer1_ where customer1_.CUSTOMER_ID=sale0_.BUYER_ID -found result:firstName=cat, lastName=inhat -found result:firstName=thing, lastName=one
Returns all Customers that are identified by a Sale
Figure 73.36. Criteria API Definition
CriteriaQuery<Store> qdef = cb.createQuery(Store.class);
Root<Store> s = qdef.from(Store.class);
s.join("sales");
qdef.select(s)
.where(cb.equal(s.get("name"), "Big Al's"));
A normal JOIN (implicit or explicit) may honor the fetch=LAZY property setting of the relation
Can be exactly what is desired
Can also cause problems or extra work if not desired
Figure 73.37. In Programming Context
@Entity @Table(name="ORMQL_STORE")
public class Store {
...
@OneToMany(mappedBy="store",
cascade={CascadeType.REMOVE},
fetch=FetchType.LAZY)
private List<Sale> sales = new ArrayList<Sale>();
Sales are lazily fetched when obtaining Store
Figure 73.38. In Programming Context (con.t)
CriteriaBuilder cb = em2.getCriteriaBuilder();
CriteriaQuery<Store> qdef = cb.createQuery(Store.class);
Root<Store> s = qdef.from(Store.class);
s.join("sales");
qdef.select(s)
.where(cb.equal(s.get("name"), "Big Al's"));
Store store = em2.createQuery(qdef).getSingleResult();
em2.close();
try {
store.getSales().get(0).getAmount();
fail("did not trigger lazy initialization exception");
} catch (LazyInitializationException expected) {
log.info("caught expected exception:" + expected);
}
Accessing the Sale properties causes a LazyInitializationException when persistence context no longer active or accessible
Figure 73.39. Runtime Output
select store0_.STORE_ID as STORE1_0_, store0_.name as name0_ from ORMQL_STORE store0_ inner join JPAQL_SALE sales1_ on store0_.STORE_ID=sales1_.SALE_STORE where store0_.name=? limit ? -caught expected exception:org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: ejava.jpa.examples.query.Store.sales, no session or session was closed
Note that only a single row is required to be returned from the database for a fetch=LAZY relation. Although it requires more queries to the database, it eliminates duplicate parent information for each child row and can eliminate the follow-on query all together when not accessed.
Figure 73.41. Criteria API Definition
CriteriaQuery<Store> qdef = cb.createQuery(Store.class);
Root<Store> s = qdef.from(Store.class);
s.fetch("sales");
qdef.select(s)
.where(cb.equal(s.get("name"), "Big Al's"));
A JOIN FETCH used to eager load related entities as side-effect of query
Can be used as substitute for fetch=EAGER specification on relation
Figure 73.42. Runtime Output
select store0_.STORE_ID as STORE1_0_0_, sales1_.SALE_ID as SALE1_1_1_, store0_.name as name0_0_, sales1_.amount as amount1_1_, sales1_.BUYER_ID as BUYER3_1_1_, sales1_.date as date1_1_, sales1_.SALE_STORE as SALE5_1_1_, sales1_.SALE_STORE as SALE5_0_0__, sales1_.SALE_ID as SALE1_0__ from ORMQL_STORE store0_ inner join JPAQL_SALE sales1_ on store0_.STORE_ID=sales1_.SALE_STORE where store0_.name=?
Sales are eagerly fetched when obtaining Store
Note that adding JOIN FETCH to parent query causes the parent rows to be repeated for each eagerly loaded child row and eliminated by the provider. This requires fewer database queries but results in more (and redundant) data to be returned from the query.
Figure 73.44. Criteria API Definition
CriteriaQuery<String> qdef = cb.createQuery(String.class);
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c.<String>get("lastName"))
.distinct(true);
Limits output to unique value combinations
Figure 73.45. Runtime Output
select distinct customer0_.LAST_NAME as col_0_0_ from JPAQL_CUSTOMER customer0_ -found result:two -found result:inhat -found result:one
Figure 73.47. Criteria API Definition
CriteriaQuery<String> qdef = cb.createQuery(String.class);
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c.<String>get("firstName"))
.distinct(true);
Figure 73.48. Runtime Output
select distinct customer0_.FIRST_NAME as col_0_0_ from JPAQL_CUSTOMER customer0_ -found result:cat -found result:thing
Figure 74.2. Criteria API Definition
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c)
.where(cb.equal(c.get("firstName"), "cat"));
Return entities where there is an equality match
Figure 74.3. Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=? -found result:firstName=cat, lastName=inhat
JPAQL requires special characters to be escaped
Figure 74.5. Criteria API Definition
CriteriaQuery<Store> qdef = cb.createQuery(Store.class);
Root<Store> s = qdef.from(Store.class);
qdef.select(s)
.where(cb.equal(s.get("name"), "Big Al's"));
Literal values automatically escaped
Figure 74.6. Runtime Output
select store0_.STORE_ID as STORE1_0_, store0_.name as name0_ from ORMQL_STORE store0_ where store0_.name=? ... -found result:name=Big Al's, sales(2)={1, 2, }
Figure 74.8. Criteria API Definition
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.like(c.<String>get("firstName"), "M%"));
Figure 74.9. Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where clerk0_.FIRST_NAME like ? ... -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, } -found result:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }
Figure 74.11. Criteria API Definition
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.like(c.<String>get("firstName"),
cb.parameter(String.class, "firstName")));
Figure 74.12. In Programming Context
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.like(c.<String>get("firstName"),
cb.parameter(String.class, "firstName")));
TypedQuery<Clerk> query = em.createQuery(qdef)
.setParameter("firstName", "M%");
List<Clerk> results = query.getResultList();
Figure 74.13. Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where clerk0_.FIRST_NAME like ? -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, } -found result:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }
Figure 74.15. Criteria API Definition
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.like(c.<String>get("firstName"),
cb.concat(cb.parameter(String.class, "firstName"), "%")));
Figure 74.16. In Programming Context
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.like(c.<String>get("firstName"),
cb.concat(cb.parameter(String.class, "firstName"), "%")));
TypedQuery<Clerk> query = em.createQuery(qdef)
.setParameter("firstName", "M");
List<Clerk> results = query.getResultList();
Figure 74.17. Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where clerk0_.FIRST_NAME like (?||?) -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, } -found result:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }
Figure 74.19. Criteria API Definition
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.like(c.<String>get("firstName"),"_anny"));
Figure 74.20. In Programming Context
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.like(c.<String>get("firstName"),"_anny"));
TypedQuery<Clerk> query = em.createQuery(qdef);
List<Clerk> results = query.getResultList();
Figure 74.21. Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where clerk0_.FIRST_NAME like ? -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, }
Figure 74.23. Criteria API Definition
CriteriaQuery<Number> qdef = cb.createQuery(Number.class);
Root<Sale> s = qdef.from(Sale.class);
qdef.select(cb.count(s))
.where(cb.greaterThan(
cb.prod(s.<BigDecimal>get("amount"), cb.parameter(BigDecimal.class, "tax")),
new BigDecimal(10.0)));
Figure 74.24. In Programming Context
CriteriaQuery<Number> qdef = cb.createQuery(Number.class);
//select count(s) from Sale s
//where (s.amount * :tax) > :amount"
Root<Sale> s = qdef.from(Sale.class);
qdef.select(cb.count(s))
.where(cb.greaterThan(
cb.prod(s.<BigDecimal>get("amount"), cb.parameter(BigDecimal.class, "tax")),
new BigDecimal(10.0)));
TypedQuery<Number> query = em.createQuery(qdef);
//keep raising taxes until somebody pays $10.00 in tax
double tax = 0.05;
for (;query.setParameter("tax", new BigDecimal(tax))
.getSingleResult().intValue()==0;
tax += 0.01) {
log.debug("tax=" + NumberFormat.getPercentInstance().format(tax));
}
log.info("raise taxes to: " + NumberFormat.getPercentInstance().format(tax));
Figure 74.25. Runtime Output
select count(*) as col_0_0_ from JPAQL_SALE sale0_ where sale0_.amount*?>10 limit ? -tax=5% select count(*) as col_0_0_ from JPAQL_SALE sale0_ where sale0_.amount*?>10 limit ? -tax=6% select count(*) as col_0_0_ from JPAQL_SALE sale0_ where sale0_.amount*?>10 limit ? -raise taxes to: 7%
Figure 74.26. Equivalent JPAQL
select c from Customer c where (c.firstName='cat' AND c.lastName='inhat') OR c.firstName='thing'
Figure 74.27. Criteria API Definition
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c)
.where(cb.or(
cb.and(cb.equal(c.get("firstName"), "cat"),
cb.equal(c.get("lastName"), "inhat")),
cb.equal(c.get("firstName"), "thing")));
Figure 74.28. Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=? and customer0_.LAST_NAME=? or customer0_.FIRST_NAME=? -found result:firstName=cat, lastName=inhat -found result:firstName=thing, lastName=one -found result:firstName=thing, lastName=two
Figure 74.29. Equivalent JPAQL
select c from Customer c where (NOT (c.firstName='cat' AND c.lastName='inhat')) OR c.firstName='thing'
Figure 74.30. Criteria API Definition
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c)
.where(cb.or(
cb.not(cb.and(cb.equal(c.get("firstName"), "cat"),
cb.equal(c.get("lastName"), "inhat"))),
cb.equal(c.get("firstName"), "thing"))
);
Figure 74.31. Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME<>? or customer0_.LAST_NAME<>? or customer0_.FIRST_NAME=? -found result:firstName=thing, lastName=one -found result:firstName=thing, lastName=two
Must compare values
Of same type
Of legal promotion type
Can compare 123:int to 123:long
Cannot compare 123:int to "123":string
Can compare entities
Figure 74.32. Equivalent JPAQL
select c from Clerk c where c.firstName = 'Manny'
select s from Sale s JOIN s.clerks c where c = :clerk
Figure 74.33. Criteria API Definition
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.equal(c.get("firstName"), "Manny"));
CriteriaQuery<Sale> qdef2 = cb.createQuery(Sale.class);
Root<Sale> s = qdef2.from(Sale.class);
Join<Sale, Clerk> c2 = s.join("clerks");
qdef2.select(s)
.where(cb.equal(c2, clerk));
Compare entities and not primary/foreign key values
Figure 74.34. In Programming Context
//find clerk of interest
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.equal(c.get("firstName"), "Manny"));
Clerk clerk = em.createQuery(qdef).getSingleResult();
//find all sales that involve this clerk
CriteriaQuery<Sale> qdef2 = cb.createQuery(Sale.class);
Root<Sale> s = qdef2.from(Sale.class);
Join<Sale, Clerk> c2 = s.join("clerks");
qdef2.select(s)
.where(cb.equal(c2, clerk));
List<Sale> sales = em.createQuery(qdef2).getResultList();
Figure 74.35. Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where clerk0_.FIRST_NAME='Manny' limit ?
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ inner join JPAQL_SALE_CLERK_LINK clerks1_ on sale0_.SALE_ID=clerks1_.SALE_ID inner join JPAQL_CLERK clerk2_ on clerks1_.CLERK_ID=clerk2_.CLERK_ID where clerk2_.CLERK_ID=? ... -found=date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, } ... -found=date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Figure 74.37. Criteria API Definition
CriteriaQuery<Sale> qdef = cb.createQuery(Sale.class);
Root<Sale> s = qdef.from(Sale.class);
qdef.select(s)
.where(cb.between(s.<BigDecimal>get("amount"),
new BigDecimal(90.00),
new BigDecimal(110.00)));
Figure 74.38. Runtime Output
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.amount between 90 and 110 ... -found=date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, }
Figure 74.40. Criteria API Definition
CriteriaQuery<Sale> qdef = cb.createQuery(Sale.class);
Root<Sale> s = qdef.from(Sale.class);
qdef.select(s)
.where(cb.not(cb.between(s.<BigDecimal>get("amount"),
new BigDecimal(90.00),
new BigDecimal(110.00))));
Figure 74.41. Runtime Output
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.amount not between 90 and 110 ... -found=date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Can be used to test for unassigned value or relationship
Figure 74.43. Criteria API Definition
CriteriaQuery<Sale> qdef = cb.createQuery(Sale.class);
Root<Sale> s = qdef.from(Sale.class);
qdef.select(s)
.where(cb.isNull(s.get("store")));
//.where(cb.equal(s.get("store"), cb.nullLiteral(Store.class)));
Figure 74.44. Runtime Output
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.SALE_STORE is null
Figure 74.46. Criteria API Definition
CriteriaQuery<Sale> qdef = cb.createQuery(Sale.class);
Root<Sale> s = qdef.from(Sale.class);
qdef.select(s)
.where(cb.isNotNull(s.get("store")));
//.where(cb.not(cb.equal(s.get("store"), cb.nullLiteral(Store.class))));
Figure 74.47. Runtime Output
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.SALE_STORE is not null ... -found result:date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, } ... -found result:date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Can be used to test for an empty collection
Figure 74.49. Criteria API Definition
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.isEmpty(c.<List<Sale>>get("sales")));
Figure 74.50. Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where not (exists ( select sale2_.SALE_ID from JPAQL_SALE_CLERK_LINK sales1_, JPAQL_SALE sale2_ where clerk0_.CLERK_ID=sales1_.CLERK_ID and sales1_.SALE_ID=sale2_.SALE_ID) ) ... -found result:firstName=Jack, lastName=Pep, hireDate=1973-03-01, termDate=null, sales(0)={}
Sub-select returns values from collection under test
Outer query tests for no existing (EMPTY)values
Figure 74.52. Criteria API Definition
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.isNotEmpty(c.<List<Sale>>get("sales")));
Figure 74.53. Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where exists ( select sale2_.SALE_ID from JPAQL_SALE_CLERK_LINK sales1_, JPAQL_SALE sale2_ where clerk0_.CLERK_ID=sales1_.CLERK_ID and sales1_.SALE_ID=sale2_.SALE_ID ) ... -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, } ... -found result:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }
Sub-select returns values from collection under test
Outer query tests for existing (NOT EMPTY)values
Can be used to determine membership in a collection
Figure 74.54. Equivalent JPAQL
select c from Clerk c where c.firstName = 'Manny'
select s from Sale s where :clerk MEMBER OF s.clerks
Figure 74.55. Criteria API Definition
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.equal(c.get("firstName"), "Manny"));
CriteriaQuery<Sale> qdef2 = cb.createQuery(Sale.class);
Root<Sale> s = qdef2.from(Sale.class);
qdef2.select(s)
.where(cb.isMember(clerk, s.<List<Clerk>>get("clerks")));
Defines a shorthand for a subquery
Figure 74.56. In Programming Context
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c)
.where(cb.equal(c.get("firstName"), "Manny"));
Clerk clerk = em.createQuery(qdef).getSingleResult();
//find all sales that involve this clerk
CriteriaQuery<Sale> qdef2 = cb.createQuery(Sale.class);
Root<Sale> s = qdef2.from(Sale.class);
qdef2.select(s)
.where(cb.isMember(clerk, s.<List<Clerk>>get("clerks")));
List<Sale> sales = em.createQuery(qdef2).getResultList();
Figure 74.57. Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where clerk0_.FIRST_NAME=? limit ?
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where ? in ( select clerk2_.CLERK_ID from JPAQL_SALE_CLERK_LINK clerks1_, JPAQL_CLERK clerk2_ where sale0_.SALE_ID=clerks1_.SALE_ID and clerks1_.CLERK_ID=clerk2_.CLERK_ID ) ... -found=date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, } ... -found=date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Useful when query cannot be expressed through JOINs
Figure 74.58. Equivalent JPAQL
select c from Customer c where c.id IN (select s.buyerId from Sale s where s.amount > 100)
Figure 74.59. Criteria API Definition
CriteriaQuery<Customer> qdef = cb.createQuery(Customer.class);
//form subquery
Subquery<Long> sqdef = qdef.subquery(Long.class);
Root<Sale> s = sqdef.from(Sale.class);
sqdef.select(s.<Long>get("buyerId"))
.where(cb.greaterThan(s.<BigDecimal>get("amount"), new BigDecimal(100)));
//form outer query
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c)
.where(cb.in(c.get("id")).value(sqdef));
Figure 74.60. Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.CUSTOMER_ID in ( select sale1_.BUYER_ID from JPAQL_SALE sale1_ where sale1_.amount>100 ) -found result:firstName=thing, lastName=one
All existing values must meet criteria (i.e., no value may fail criteria)
Zero values is the lack of failure (i.e., meets criteria)
Figure 74.61. Equivalent JPAQL
select c from Clerk c where 125 < ALL (select s.amount from c.sales s)
Figure 74.62. Criteria API Definition
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c);
Subquery<BigDecimal> sqdef = qdef.subquery(BigDecimal.class);
Root<Clerk> c1 = sqdef.from(Clerk.class);
Join<Clerk,Sale> s = c1.join("sales");
sqdef.select(s.<BigDecimal>get("amount"))
.where(cb.equal(c, c1));
Predicate p1 = cb.lessThan(
cb.literal(new BigDecimal(125)),
cb.all(sqdef));
qdef.where(p1);
List all clerks that have all sales above $125.00 or none at all
-or- List all clerks with no sale <= $125.00
Figure 74.63. Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where 125<all ( select sale3_.amount from JPAQL_CLERK clerk1_ inner join JPAQL_SALE_CLERK_LINK sales2_ on clerk1_.CLERK_ID=sales2_.CLERK_ID inner join JPAQL_SALE sale3_ on sales2_.SALE_ID=sale3_.SALE_ID where clerk0_.CLERK_ID=clerk1_.CLERK_ID ) ... -found result:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, } ... -found result:firstName=Jack, lastName=Pep, hireDate=1973-03-01, termDate=null, sales(0)={}
Manny excluded because has 1 sale below $125.00
Moe included because has only $150.00 sale
Jack included because has no sales that fail criteria
Figure 74.64. Equivalent JPAQL
select c from Clerk c where 125 > ALL (select s.amount from c.sales s)
Figure 74.65. Criteria API Definition
Predicate p2 = cb.greaterThan(
cb.literal(new BigDecimal(125)),
cb.all(sqdef));
qdef.where(p2);
List all clerks that have all sales below $125.00 or none at all
-or- List all clerks with no sale >= $125.00
Figure 74.66. Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where 125>all ( select sale3_.amount from JPAQL_CLERK clerk1_ inner join JPAQL_SALE_CLERK_LINK sales2_ on clerk1_.CLERK_ID=sales2_.CLERK_ID inner join JPAQL_SALE sale3_ on sales2_.SALE_ID=sale3_.SALE_ID where clerk0_.CLERK_ID=clerk1_.CLERK_ID ) -found result:firstName=Jack, lastName=Pep, hireDate=1973-03-01, termDate=null, sales(0)={}
Manny excluded because has 1 sale above $125.00
Moe excluded because has only $150.00 sale
Jack included because has no sales that fail criteria
Any matching value meets criteria (i.e., one match and you are in)
Zero values fails to meet the criteria (i.e., must have at least one matching value)
Figure 74.67. Equivalent JPAQL
select c from Clerk c where 125 < ANY (select s.amount from c.sales s)
Figure 74.68. Criteria API Definition
CriteriaQuery<Clerk> qdef = cb.createQuery(Clerk.class);
Root<Clerk> c = qdef.from(Clerk.class);
qdef.select(c);
//select c from Clerk c
//where 125 < ALL " +
//(select s.amount from c.sales s)",
Subquery<BigDecimal> sqdef = qdef.subquery(BigDecimal.class);
Root<Clerk> c1 = sqdef.from(Clerk.class);
Join<Clerk,Sale> s = c1.join("sales");
sqdef.select(s.<BigDecimal>get("amount"))
.where(cb.equal(c, c1));
Predicate p1 = cb.lessThan(
cb.literal(new BigDecimal(125)),
cb.any(sqdef));
qdef.where(p1);
List all clerks that have at least one sale above $125.00
Figure 74.69. Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where 125<any ( select sale3_.amount from JPAQL_CLERK clerk1_ inner join JPAQL_SALE_CLERK_LINK sales2_ on clerk1_.CLERK_ID=sales2_.CLERK_ID inner join JPAQL_SALE sale3_ on sales2_.SALE_ID=sale3_.SALE_ID where clerk0_.CLERK_ID=clerk1_.CLERK_ID ) ... -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, } ... -found result:firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }
Manny included because has 1 sale above $125.00
Moe included because $150.00 sale qualifies him as well
Jack excluded because has no sales that meet criteria
Figure 74.70. Equivalent JPAQL
select c from Clerk c where 125 > ANY (select s.amount from c.sales s)
Figure 74.71. Criteria API Definition
Predicate p2 = cb.greaterThan(
cb.literal(new BigDecimal(125)),
cb.any(sqdef));
qdef.where(p2);
List all clerks that have at least one sale below $125.00
Figure 74.72. Runtime Output
select clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ where 125>any ( select sale3_.amount from JPAQL_CLERK clerk1_ inner join JPAQL_SALE_CLERK_LINK sales2_ on clerk1_.CLERK_ID=sales2_.CLERK_ID inner join JPAQL_SALE sale3_ on sales2_.SALE_ID=sale3_.SALE_ID where clerk0_.CLERK_ID=clerk1_.CLERK_ID ) -found result:firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, }
Manny included because has 1 sale below $125.00
Moe excluded because his only $150.00 sale above criteria
Jack excluded because has no sales that meet criteria
Figure 75.2. Criteria API Definition
CriteriaQuery qdef = cb.createQuery();
Root<Customer> c = qdef.from(Customer.class);
qdef.select(c)
.where(cb.equal(c.get("firstName"),"CAT"));
Using an untyped CriteriaQuery to be able to switch between different query output types within example
Figure 75.3. Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=?
No rows found because 'CAT' does not match anything because of case
Figure 75.5. Criteria API Definition
qdef.select(c)
.where(cb.equal(c.get("firstName"),cb.lower(cb.literal("CAT"))));
Figure 75.6. Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=lower(?) -found result:firstName=cat, lastName=inhat
One customer found because case-sensitive compare now correct
Figure 75.7. Equivalent JPAQL
select UPPER(c.firstName) from Customer c where c.firstName=LOWER('CAT')
Figure 75.8. Criteria API Definition
qdef.select(cb.upper(c.<String>get("firstName")))
.where(cb.equal(c.get("firstName"),cb.lower(cb.literal("CAT"))));
Figure 75.9. Runtime Output
select upper(customer0_.FIRST_NAME) as col_0_0_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=lower(?) -found result:CAT
First name of customer located returned in upper case
Figure 75.10. Equivalent JPAQL
select TRIM(LEADING 'c' FROM c.firstName) from Customer c where c.firstName='cat')
Figure 75.11. Criteria API Definition
qdef.select(cb.trim(Trimspec.LEADING, 'c', c.<String>get("firstName")))
.where(cb.equal(c.get("firstName"),"cat"));
Figure 75.12. Runtime Output
select trim(LEADING ? from customer0_.FIRST_NAME) as col_0_0_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=? -found result:at
Customer's name, excluding initial 'c' character, returned
Figure 75.13. Equivalent JPAQL
select c from Customer c where CONCAT(CONCAT(c.firstName,' '),c.lastName) ='cat inhat')
Figure 75.14. Criteria API Definition
qdef.select(c)
.where(cb.equal(
cb.concat(
cb.concat(c.<String>get("firstName"), " "),
c.<String>get("lastName")),
"cat inhat"));
Figure 75.15. Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where (customer0_.FIRST_NAME||?||customer0_.LAST_NAME)=? -found result:firstName=cat, lastName=inhat
Customer located after concatenation of fields yields match
Figure 75.17. Criteria API Definition
qdef.select(c)
.where(cb.equal(cb.length(c.<String>get("firstName")),3));
Figure 75.18. Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where length(customer0_.FIRST_NAME)=3 -found result:firstName=cat, lastName=inhat
Customer found where length of firstName matches specified length criteria
Figure 75.20. Criteria API Definition
qdef.select(c)
.where(cb.greaterThan(cb.locate(c.<String>get("firstName"), "cat", 2),0));
Figure 75.21. Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where locate(?, customer0_.FIRST_NAME, 2)>0
No firstName found with 'cat' starting at position=2
Figure 75.22. Equivalent JPAQL
select c from Customer c where LOCATE('at',c.firstName,2) > 1
qdef.select(c)
.where(cb.greaterThan(cb.locate(c.<String>get("firstName"), "at", 2),1));
Figure 75.23. Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where locate(?, customer0_.FIRST_NAME, 2)>1 -found result:firstName=cat, lastName=inhat
firstName found with 'at' starting at a position 2
Figure 75.24. Equivalent JPAQL
select SUBSTRING(c.firstName,2,2) from Customer c where c.firstName = 'cat'
Figure 75.25. Criteria API Definition
qdef.select(cb.substring(c.<String>get("firstName"), 2, 2))
.where(cb.equal(c.get("firstName"), "cat"));
Figure 75.26. Runtime Output
select substring(customer0_.FIRST_NAME, 2, 2) as col_0_0_ from JPAQL_CUSTOMER customer0_ where customer0_.FIRST_NAME=? -found result:at
Return the two character substring of firstName starting at position two
Figure 75.28. Criteria API Definition
qdef.select(c)
.where(cb.equal(
cb.substring(c.<String>get("firstName"), 2, 2),
"at"));
Figure 75.29. Runtime Output
select customer0_.CUSTOMER_ID as CUSTOMER1_3_, customer0_.FIRST_NAME as FIRST2_3_, customer0_.LAST_NAME as LAST3_3_ from JPAQL_CUSTOMER customer0_ where substring(customer0_.FIRST_NAME, 2, 2)=? -found result:firstName=cat, lastName=inhat
Find the customer with a two characters starting a position two of firstName equaling 'at'
CriteriaBuilder.currentDate()
CriteriaBuilder.currentTime()
CriteriaBuilder.currentTimestamp()
Figure 75.31. Criteria API Definition
CriteriaQuery<Sale> qdef = cb.createQuery(Sale.class);
Root<Sale> s = qdef.from(Sale.class);
qdef.select(s);
qdef.where(cb.lessThan(s.<Date>get("date"), cb.currentDate()));
Figure 75.32. Runtime Output
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.date<current_date() ... -found result:date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, } ... -found result:date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Located two Sales that occurred prior to today's date
Figure 75.35. Runtime Output
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.date=current_date()
Located no sales on today's date
Update with a bulk query
Criteria API added Bulk Updates in JPA 2.1
Figure 75.37. Criteria API Definition
CriteriaUpdate<Sale> qupdate = cb.createCriteriaUpdate(Sale.class);
Root<Sale> s2 = qupdate.from(Sale.class);
qupdate.set(s2.<Date>get("date"), cb.currentDate());
int rows = em.createQuery(qupdate).executeUpdate();
Update all sales to today
Figure 75.41. Runtime Output
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ where sale0_.date=current_date() ... -found result:date=2013-06-05 00:00:00, amount=$100.00, buyer=1, clerks(1)={1, } ... -found result:date=2013-06-05 00:00:00, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Now locating sales for today's date
Bulk commands (i.e., update) invalidate cached entities. You must refresh their state with the database or detach/clear them from the persistence context to avoid using out-dated information.
ASC - ascending order
DESC - descending order
Figure 75.43. Criteria API Definition
CriteriaQuery<Sale> qdef = cb.createQuery(Sale.class);
Root<Sale> s = qdef.from(Sale.class);
qdef.select(s);
qdef.orderBy(cb.asc(s.get("amount")));
Figure 75.44. Runtime Output
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ order by sale0_.amount ASC ... -found result:date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, } ... -found result:date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, }
Note the ASC order on amount
Figure 75.47. Runtime Output
select sale0_.SALE_ID as SALE1_1_, sale0_.amount as amount1_, sale0_.BUYER_ID as BUYER3_1_, sale0_.date as date1_, sale0_.SALE_STORE as SALE5_1_ from JPAQL_SALE sale0_ order by sale0_.amount DESC -found result:date=1999-06-11 14:15:10, amount=$150.00, buyer=2, clerks(2)={1, 2, } -found result:date=1998-04-10 10:13:35, amount=$100.00, buyer=1, clerks(1)={1, }
Note the DESC order on amount
Figure 75.49. Criteria API Definition
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Number> qdef = cb.createQuery(Number.class); Root<Sale> s = qdef.from(Sale.class); qdef.select(cb.count(s));
Figure 75.52. Criteria API Definition
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Number> qdef = cb.createQuery(Number.class); Root<Sale> s = qdef.from(Sale.class); qdef.select(cb.min(s.<BigDecimal>get("amount")));
Figure 75.53. Runtime Output
select min(sale0_.amount) as col_0_0_ from JPAQL_SALE sale0_ -found result:100.00
Figure 75.55. Criteria API Definition
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Number> qdef = cb.createQuery(Number.class); Root<Sale> s = qdef.from(Sale.class); qdef.select(cb.max(s.<BigDecimal>get("amount")));
Figure 75.56. Runtime Output
select max(sale0_.amount) as col_0_0_ from JPAQL_SALE sale0_ -found result:150.00
Figure 75.58. Criteria API Definition
CriteriaQuery<Number> qdef = cb.createQuery(Number.class); Root<Sale> s = qdef.from(Sale.class); //select sum(s.amount) from Sale s qdef.select(cb.sum(s.<BigDecimal>get("amount")));
Figure 75.59. Runtime Output
select sum(sale0_.amount) as col_0_0_ from JPAQL_SALE sale0_ -found result:250.0
Figure 75.61. Criteria API Definition
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Number> qdef = cb.createQuery(Number.class); Root<Sale> s = qdef.from(Sale.class); qdef.select(cb.avg(s.<BigDecimal>get("amount")));
Figure 75.62. Runtime Output
select avg(cast(sale0_.amount as double)) as col_0_0_ from JPAQL_SALE sale0_ -found result:125.0
Figure 75.64. Criteria API Definition
CriteriaQuery<Object[]> qdef = cb.createQuery(Object[].class);
Root<Clerk> c = qdef.from(Clerk.class);
Join<Clerk,Sale> s = c.join("sales", JoinType.LEFT);
qdef.select(cb.array(c, cb.count(s)))
.groupBy(c);
Get count of sales for each clerk
Figure 75.65. Runtime Output
select clerk0_.CLERK_ID as col_0_0_, count(sale2_.SALE_ID) as col_1_0_, clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ left outer join JPAQL_SALE_CLERK_LINK sales1_ on clerk0_.CLERK_ID=sales1_.CLERK_ID left outer join JPAQL_SALE sale2_ on sales1_.SALE_ID=sale2_.SALE_ID group by clerk0_.CLERK_ID ... -found=[firstName=Manny, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(2)={1, 2, }, 2] ... -found=[firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }, 1] ... -found=[firstName=Jack, lastName=Pep, hireDate=1973-03-01, termDate=null, sales(0)={}, 0]
Figure 75.66. Equivalent JPAQL
select c, COUNT(s) from Clerk c LEFT JOIN c.sales s GROUP BY c HAVING COUNT(S) <= 1
Figure 75.67. Criteria API Definition
CriteriaQuery<Object[]> qdef = cb.createQuery(Object[].class);
Root<Clerk> c = qdef.from(Clerk.class);
Join<Clerk,Sale> s = c.join("sales", JoinType.LEFT);
qdef.select(cb.array(c, cb.count(s)))
.groupBy(c)
.having(cb.le(cb.count(s), 1));
Provide a list of Clerks and their count of Sales for counts <= 1
Figure 75.68. Runtime Output
select clerk0_.CLERK_ID as col_0_0_, count(sale2_.SALE_ID) as col_1_0_, clerk0_.CLERK_ID as CLERK1_2_, clerk0_.FIRST_NAME as FIRST2_2_, clerk0_.HIRE_DATE as HIRE3_2_, clerk0_.LAST_NAME as LAST4_2_, clerk0_.TERM_DATE as TERM5_2_ from JPAQL_CLERK clerk0_ left outer join JPAQL_SALE_CLERK_LINK sales1_ on clerk0_.CLERK_ID=sales1_.CLERK_ID left outer join JPAQL_SALE sale2_ on sales1_.SALE_ID=sale2_.SALE_ID group by clerk0_.CLERK_ID having count(sale2_.SALE_ID)<=1 ... -found=[firstName=Moe, lastName=Pep, hireDate=1970-01-01, termDate=null, sales(1)={2, }, 1] ... -found=[firstName=Jack, lastName=Pep, hireDate=1973-03-01, termDate=null, sales(0)={}, 0]
Wed matched on Moe (1 sale) and Jack (0 sales)
Copyright © 2019 jim stafford (jim.stafford@jhu.edu)
Built on: 2019-08-22 07:10 EST
Abstract
This presentation provides information for JPA/SQL developers to better understand how database constructs and database access decisions can impact application performance. It provides a brief discussion of tools that can be used and how to review an execution plan. Relative comparisons between approaches are provided to help show the costs and benefits of different approaches.
Table of Contents
Why DB access can be inefficient
What is an execution plan
How to access an execution plan
How to analyze an execution plan
Impact of Indexes
to access tables
to access joins
JPA performance pitfalls
The material in this paper/presentation is geared towards making JPA/SQL developers more aware of issues that can impact performance in the applications they develop. The material in this paper is not intended to be a performance benchmark for any database or JPA provider -- thus the environment used is not described (and quite far from production standards). Use all reported measurements as relative across the alternatives within the example they are presented. The material covered in this paper should be an excellent start for developers to better understand what costs what, how you can better know when there are issues or success, and where to look for solutions. Some topics were purposely left out to help concentrate on the primary scenarios.
Poorly constructed data model
Poorly constructed access for the data model
Missing access structures
Indexes
Materialized views
Missing constraints
Unique Constraints
NOT null
Sub-optimal execution plan
Incorrect estimates of cost, cardinality, or predictive selectivity
Stale or missing optimizer statistics
Output of the optimizer
Instructions of what execution engine must perform to complete query
Parent-child relationship between steps showing
Ordering of tables referenced
Table access methods used
Join methods used
Data operations (e.g., filter, sort, aggregation)
Cardinality = number of rows selected (based on count of unique columns)
Cost = disk access, I/O and CPU cost estimates for number of rows selected
Partition access
Parallel execution
Figure 76.1. JUnit Benchmark Tool
NoColumnIndex.queryForMovieAndDir: [measured 1 out of 1 rounds, threads: 1 (sequential)] round: 3.85 [+- 0.00], round.block: 0.00 [+- 0.00], round.gc: 0.00 [+- 0.00], GC.calls: 0, GC.time: 0.00, time.total: 3.85, time.warmup: 0.00, time.bench: 3.85
Helps you determine which execution plan selected by optimizer
Statement is not executed
Plan is theoretical
Figure 76.3. EXPLAIN PLAN Example using Text SQL Commands
EXPLAIN PLAN FOR select * from ( select movie0_.TITLE as col_0_0_, person2_.FIRST_NAME as col_1_0_, person2_.LAST_NAME as col_2_0_ from JPATUNE_MOVIE movie0_ inner join JPATUNE_DIRECTOR director1_ on movie0_.DIRECTOR_ID=director1_.PERSON_ID inner join JPATUNE_PERSON person2_ on director1_.PERSON_ID=person2_.ID order by title DESC ) where rownum <= :limit; SET LINESIZE 100 SET PAGESIZE 0 select * from table(DBMS_XPLAN.DISPLAY());
plan FOR succeeded. Plan hash value: 857441453 ---------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | ---------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 774K| 477M| | 25766 (1)| 00:05:10 | |* 1 | COUNT STOPKEY | | | | | | | | 2 | VIEW | | 774K| 477M| | 25766 (1)| 00:05:10 | |* 3 | SORT ORDER BY STOPKEY | | 774K| 45M| 53M| 25766 (1)| 00:05:10 | |* 4 | HASH JOIN | | 774K| 45M| 11M| 14333 (1)| 00:02:52 | |* 5 | HASH JOIN | | 271K| 8746K| 5568K| 5115 (1)| 00:01:02 | | 6 | INDEX FAST FULL SCAN| DIRECTOR_PK | 271K| 2385K| | 191 (1)| 00:00:03 | | 7 | TABLE ACCESS FULL | JPATUNE_PERSON | 1637K| 37M| | 1854 (1)| 00:00:23 | |* 8 | TABLE ACCESS FULL | JPATUNE_MOVIE | 774K| 20M| | 7169 (1)| 00:01:27 | ---------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter(ROWNUM<=TO_NUMBER(:LIMIT)) 3 - filter(ROWNUM<=TO_NUMBER(:LIMIT)) 4 - access("MOVIE0_"."DIRECTOR_ID"="DIRECTOR1_"."PERSON_ID") 5 - access("DIRECTOR1_"."PERSON_ID"="PERSON2_"."ID") 8 - filter("MOVIE0_"."DIRECTOR_ID" IS NOT NULL)
Figure 76.4. EXPLAIN PLAN Example using Named Plan
EXPLAIN PLAN SET STATEMENT_ID='myplan01' FOR select * from ( select movie0_.TITLE as col_0_0_, person2_.FIRST_NAME as col_1_0_, person2_.LAST_NAME as col_2_0_ ...
select * from table(DBMS_XPLAN.DISPLAY('PLAN_TABLE','myplan01','typical',null));
Produces execution plan and statistics after running statement(s)
Statement(s) are actually run -- not theoretical like EXPLAIN PLAN
grant SELECT_CATALOG_ROLE to (user); grant SELECT ANY DICTIONARY to (user);
Runtime cursors store execution plans within V$PLAN
DISPLAY_CURSOR requires select privileges on: V$SQL_PLAN, V$SESSION and V$SQL_PLAN_STATISTICS_ALL
Figure 76.6. Example: Connection Generates SQL Commands
select poo.first_name, poo.last_name from jpatune_person poo;
Figure 76.7. Example: SQL Commands of Interest Located in V$PLAN
select sql_id, sql_fulltext from V$SQL where sql_fulltext like '%from jpatune_person poo%';
87d246wux9qag "select poo.first_name, poo.last_name from jpatune_person poo" 9bzam4xu5q7p5 select sql_id, sql_fulltext from V$SQL where sql_fulltext like '%from jpatune_person poo%'
Figure 76.8. Example: SQL_ID from V$PLAN Used to Display Execution Plan
select PLAN_TABLE_OUTPUT from TABLE(DBMS_XPLAN.DISPLAY_CURSOR('87d246wux9qag',null, 'TYPICAL'));
SQL_ID 87d246wux9qag, child number 0 ------------------------------------- select poo.first_name, poo.last_name from jpatune_person poo Plan hash value: 1628338048 ------------------------------------------------------------------------------------ | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | | | 3 (100)| | | 1 | TABLE ACCESS FULL| JPATUNE_PERSON | 10 | 680 | 3 (0)| 00:00:01 | ------------------------------------------------------------------------------------ Note ----- - dynamic sampling used for this statement (level=2)
Figure 76.9. Example: DISPLAY_CURSOR witin Single Command
SELECT t.* FROM v$sql s, table(DBMS_XPLAN.DISPLAY_CURSOR(s.sql_id, s.child_number)) t WHERE sql_text LIKE '%JPATUNE_MOVIEGENRE%';
Access paths - strategies used to access data within table
Indexes - good for when accessing small amounts of data out of much larger data set
Full table scans - good for small tables and unrestricted row access
Any row can be *functionally* accessed with either method
Use execution plans to help use the appropriate technique
Database engine derives all information from the table directly from the physical storage location for the table. Table storage is not arranged in any guaranteed order.
Request N rows of data from a table with no defined order
Full Table Scan selected when:
There is no where clause
There is no suitable index
calling functions on row data (ex. upper(title)='TREMORS')
comparing column against data type requiring conversion
using wildcard in leading character (ex. title like '%emors')
searching for null column values (ex. rating is null) with single column indexes
Full Table Scan optimal when:
Using small tables
Majority of the rows in a larger table must be read (e.g., no where clause)
Figure 78.2. Generated SQL
select * from ( select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ ) where rownum <= ?
DB scans table as a part of getting data to resolve select clause
No added cost for where or order-by clauses
Figure 78.4. Relative Test Result
Table Access without Index.Unrestricted Scan:warmups=2, rounds=11, ave=0.73 [+- 0.03]
Since the DB's job in this case is to obtain data from all rows of the table in any order -- an index would not add value.
Request N rows of data matching a condition on an un-indexed column
Figure 78.5. JPA Query
"select m from Movie m where m.rating = :rating", params={rating=R}, limit=1000
Figure 78.6. Generated SQL
select * from ( select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ where movie0_.RATING=? ) where rownum <= ?
Where constraint adds cost to unconstrained query
DB must scan the rows in the table for a match to query
Figure 78.8. Relative Test Result
Table Access without Index.Value Access:warmups=2, rounds=11, ave=0.80 [+- 0.04]
Full table scans are the most inefficient way to obtain certain rows from a large DB table.
Request N rows of data matching a condition on an indexed column
Uses an exact location for a row -- normally obtained from an index
Fastest way to locate an individual row in a table
Note: rows can be moved during an update -- requiring multiple locations to be accessed when using former rowId
Figure 78.10. JPA Query
"select m from Movie m where m.rating = :rating", params={rating=R}, limit=1000
Figure 78.11. Generated SQL
select * from ( select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ where movie0_.RATING=? ) where rownum <= ?
Index on where clause search term avoids scanning table -- index scanned instead
DB row is accessed to satisfy select clause
DB row is located by rowId from index
Figure 78.13. Relative Test Result
Table Access with Single Indexes.Value Access:warmups=2, rounds=11, ave=0.75 [+- 0.03]
An index will speed access to rows within a table when selecting a small subset of a larger table.
Request N rows of data matching a condition on an indexed column invalidated by a function
Figure 78.15. JPA Query
"select m from Movie m where upper(m.rating) = :rating", params={rating=R}, limit=1000
Figure 78.16. Generated SQL
select * from ( select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ where upper(movie0_.RATING)=? ) where rownum <= ?
Applying function to column indexed by value invalidates use of index
DB reverts to full table scan to locate row
Figure 78.18. Relative Test Result
Table Access with Single Indexes.Unindexed Function Access:warmups=2, rounds=11, ave=0.79 [+- 0.04]
Calling a function on a DB column within the where clause will invalidate the use of an index on that column -- unless it is a function-based index (matching what is being used in the where)
Request N rows of data matching a functional condition on a function-indexed column
Figure 78.20. JPA Query
"select m from Movie m where lower(m.rating) = :rating", params={rating=r}, limit=1000
Figure 78.21. Generated SQL
select * from ( select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ where lower(movie0_.RATING)=? ) where rownum <= ?
Where clause satisfied by scanning function index
DB row accessed to satisfy select clause
DB row accessed by RowId
COST is same order of magnitude (but )as non-function index
Figure 78.23. Relative Test Result
Table Access with Single Indexes.Indexed Function Access:warmups=2, rounds=11, ave=0.76 [+- 0.04]
The DB will store the pre-calculated value in the index for use during the query.
Request N rows of data matching a like condition containing a leading wildcard
Figure 78.25. JPA Query
"select m from Movie m where m.title like :title", params={title=%eventeen: The Faces for Fall}, limit=1000
Figure 78.26. Generated SQL
select * from ( select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ where movie0_.TITLE like ? ) where rownum <= ?
Row located using full table scan
Leading wildcard in like invalidates use of indexed column
Figure 78.28. Relative Test Result
Table Access with Single Indexes.Ending Wildcard:warmups=2, rounds=11, ave=0.22 [+- 0.04] Table Access with Single Indexes.Exact Equals:warmups=2, rounds=11, ave=0.20 [+- 0.02] Table Access with Single Indexes.Exact Like:warmups=2, rounds=11, ave=0.22 [+- 0.05] Table Access with Single Indexes.Leading Wildcard:warmups=2, rounds=11, ave=0.66 [+- 0.05]
Leading wildcards invalidate the use of column indexes. Indexes are still used with non-leading wildcards.
Order returned results by one or more columns
Request N rows of data ordered by (ASC) a column
Figure 78.31. Generated SQL
select * from ( select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ order by movie0_.TITLE ASC ) where rownum <= ?
DB table is scanned as before when unconstrained by where clause
Output is sorted at a noticeable cost increase
Figure 78.33. Relative Test Result
Table Access with Single Indexes.OrderBy Non-Null Column:warmups=2, rounds=11, ave=0.76 [+- 0.03]
Look to reduce the number of rows required to be sorted or to use an index (see next topic).
Request N rows of data ordered by (ASC) indexed column (ASC) from where
Figure 78.35. JPA Query
"select m from Movie m where m.title like :title order by title ASC", params={title=A%}, limit=1000
Figure 78.36. Generated SQL
select * from ( select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ where movie0_.TITLE like ? order by movie0_.TITLE ASC ) where rownum <= ?
Range scan of index is used to satisfy where clause
Order of index used to satisfy order by clause
Cost significantly less that sort
Figure 78.38. Relative Test Result
Table Access with Single Indexes.OrderBy Non-null Where Column ASC:warmups=2, rounds=11, ave=0.69 [+- 0.02]
It is more efficient to sort results by the indexed values in the where clause because the index is already maintained in sort order.
Request N rows of data ordered by (DESC) indexed column (ASC) from where
Figure 78.40. JPA Query
"select m from Movie m where m.title like :title order by title DESC", params={title=A%}, limit=1000
Figure 78.41. Generated SQL
select * from ( select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ where movie0_.TITLE like ? order by movie0_.TITLE DESC ) where rownum <= ?
Normal (ASC) indexes can be efficiently traversed in DESC order
Figure 78.43. Relative Test Result
Table Access with Single Indexes.OrderBy Non-null Where Column DESC:warmups=2, rounds=11, ave=0.72 [+- 0.04]
Request N rows of data ordered by (DESC) indexed column (DESC) from where
Figure 78.45. JPA Query
"select m from Movie m where m.title like :title order by title DESC", params={title=A%}, limit=1000
Figure 78.46. Generated SQL
select * from ( select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ where movie0_.TITLE like ? order by movie0_.TITLE DESC ) where rownum <= ?
DESC index implementedas a function index
Function indexes are not handled as efficiently as normal indexes ["...cardinality estimates are not as accurate and the SYS_OP_DESCEND and SYS_OP_UNDESCEND functions are used as access/filter conditions as they’re the functions implemented in the function-based index" http://richardfoote.wordpress.com/category/descending-indexes/]
Figure 78.48. Relative Test Result
Table Access with Single Indexes DESC.OrderBy Non-null Where Column DESC:warmups=2, rounds=11, ave=0.99 [+- 0.03]
Ascending indexes can be efficiently traversed in ascending and descending order.
Full table scans efficient when required to access most rows
Indexes access more efficient when selecting small subset of table
Indexes can be invalidated - resulting in full table scans
using functions on columns (without a function index)
using leading wildcards
searching/counting for null column values (single column indexes do not store null values)
Indexes can be used to bypass expensive sort operations. Either:
Column must be non-nullable
Where clause excludes null values
Can be unique or non-unique
Can be simple or composite
Can be normal (ascending) or descending
Can be reverse key (for monotonically incrementing column values) to balance B*-tree
Can be function-based (to address normalization uses)
Can be used to implement sort for "order by"
Can be used to implement the entire table (Index-organized Table(IOT))
Can be traversed in different ways
Unique Scan - used with "unique" or "primary key" indexes to return a single value
Range Scan - used with "unique" and "non-unique" indexes to return multiple matching rows
Full Scan - used with composite indexes where leading where column is not part of index (i.e., can use col2 of composite)
Fast Full Scan - used when all columns of query (where and select) are contained within composite index -- table is skipped
...
Can be coalesced or rebuilt with
ALTER INDEX (index-name) (COALESCE | REBUILD)
Coalesce - repairs index in place. Good for small repairs
Rebuild - totally rebuilds index. Good for large repairs
Request N rows matching a specific criteria using a non-unique index
Figure 79.1. DB Index(es)
alter table jpatune_movie add utitle varchar2(256) update jpatune_movie set utitle=concat(concat(concat(title,'('),id),')') create index movie_utitle_idx on jpatune_movie(utitle)
* "utitle" was added as the concatenation of title(id) to create a unique column.
** we are not yet taking advantage of the uniqueness of the column
Figure 79.2. JPA Query
"select m from Movie m where m.title = :title", params={title=Tremors(m836199)}
Figure 79.3. Generated SQL
select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.utitle as utitle6_2_ from JPATUNE_MOVIE movie0_ where movie0_.utitle=?
Matching row located using index
Since index is non-unique multiple entries must be scanned
Figure 79.5. Relative Test Result
Non-Unique Index.Values By Index:warmups=2, rounds=11, ave=0.17 [+- 0.01]
Request N rows matching a specific criteria using a unique index
* we are now taking advantage of the unique column values.
Figure 79.7. JPA Query
"select m from Movie m where m.title = :title", params={title=Tremors(m836199)}
Figure 79.8. Generated SQL
select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.utitle as utitle6_2_ from JPATUNE_MOVIE movie0_ where movie0_.utitle=?
Index used to locate table rowId
Unique index scan used since index contains unique values
Figure 79.10. Relative Test Result
Nullable Unique Index.Values By Index:warmups=2, rounds=10, ave=0.18 [+- 0.01]
* the measurements in this test do not prove the following assertion which was based on the execution plan.
DB knows there can be only a single match and stops index scan after first match found
Indexes with multiple columns to match the where clause and optionally the select and join clauses as well.
Selecting N columns for M rows matching two (2) criteria terms
Figure 79.11. JPA Query
"select m from Movie m where m.title = :title and m.releaseDate = :releaseDate", params={title=Apollo 13, releaseDate=1995-07-01}
Figure 79.12. Generated SQL
select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ where movie0_.TITLE=? and movie0_.RELEASE_DATE=?
First term of a two term where clause indexed
Candidate rows located using a range scan of the index on first condition
Final rows filtered from candidate rows using second condition
Figure 79.15. Relative Test Result
Dual Indexes.Get By Term1 and Term2:warmups=2, rounds=11, ave=0.21 [+- 0.01]
DB will re-order the query to try to take advantage of an index that will result in the least number of rows moving forward to next step.
All terms in the where clause part of same composite index
Rows located using range scan of the composite index, applying the two conditions
Select clause satisfied using table rows accessed by rowId
Figure 79.18. Relative Test Result
Compound Index.Get By Term1 and Term2:warmups=2, rounds=11, ave=0.25 [+- 0.02]
Adding all terms from the where clause may add some efficiency over a single term index -- but not a significant upgrade from a single index when the contents of the row must be returned anyway.
Incorporate terms from select clause into composite index to bypass table access.
Selecting N columns from M rows matching Q criteria terms
Figure 79.19. JPA Query
"select m.rating from Movie m where m.title like :title", params={title=A%}, limit=2000
Figure 79.20. Generated SQL
select * from ( select movie0_.RATING as col_0_0_ from JPATUNE_MOVIE movie0_ where movie0_.TITLE like ? ) where rownum <= ?
Where clause satisfied using an index range scan
Select clause satisfied using table access by rowId from index
Figure 79.23. Relative Test Result
Where Column Index.Query for Values:warmups=2, rounds=11, ave=1.25 [+- 0.06]
* this index contains columns from both the where and select clauses
Where clause satisfied using an index range scan
Select clause satisfied using same index since composite index also contains all necessary columns
Figure 79.26. Relative Test Result
Where, Select Index.Query for Values:warmups=2, rounds=11, ave=1.26 [+- 0.06]
DB will bypass rowId access to row if index already contains all columns necessary to satisfy the select clause.
DB can make use of secondary terms within composite index.
* this index is the mirror image of previous example and contains columns from both the select and where clauses
Index did not start with terms from where clause, but usable
Where clause satisfied using a fast full scan of composite index
Select clause satisfied using same index since composite index also contains all necessary columns
Figure 79.29. Relative Test Result
Select, Where Index.Query for Values:warmups=2, rounds=11, ave=1.35 [+- 0.04]
Consider leveraging secondary columns of an existing composite index (over adding another index) to satisfy low-priority queries.
Indexes speed access to specific rows
Indexes should match where clause of high priority queries
Composite indexes can be formed with multiple terms from the where and select clause
Secondary columns of composite indexes can be used (over creating additional index) in lower priority queries
Unique indexes can be searched faster than non-unique indexes -- take advantage of unique column values
Improving performance of foreign key joins.
Select N properties for object M matching certain criteria
Figure 80.1. JPA Query
"select r from MovieRole r join r.movie m where m.title=:title and m.releaseDate=:releaseDate", params={title=Tremors, releaseDate=1990-07-01}
Figure 80.2. Generated SQL
select movierole0_.ID as ID1_3_, movierole0_.ACTOR_ID as ACTOR3_3_, movierole0_.MOVIE_ID as MOVIE4_3_, movierole0_.MOVIE_ROLE as MOVIE2_3_ from JPATUNE_MOVIEROLE movierole0_ inner join JPATUNE_MOVIE movie1_ on movierole0_.MOVIE_ID=movie1_.ID where movie1_.TITLE=? and movie1_.RELEASE_DATE=?
Perform query without any index support for where, select, or join clauses
Full table scan of Movie to satisfy where clause and to obtain movie_id for join
Full table scan of MovieRole to locate MovieRoles.movie_id and to obtain rows to satisfy select clause
* Large portion of cost spent looking for children
The cost of performing a full table scan of the child table looking for children to match a specific parent is magnified when the overall query selects multiple parent rows. The child table must be scanned for each parent.
Address the biggest cost item in the execution plan by adding an index on the foreign key column.
Full table scan of Movie to satisfy where clause and to obtain movie_id for join
Range scan of foreign key index used to locate MovieRoles.movie_id
MovieRole rows accessed by rowId to satisfy select clause
* Eliminated much of the cost to locate children. Still have cost to locate parent
Add indexes to foreign key columns of child tables when the child table is frequently joined with parent rows. The index will go unused if the child is simply searched alone.
Figure 80.8. DB Index(es)
create index movie_title_rdate_idx on jpatune_movie(title, release_date) create index movierole_movie_fkx on jpatune_movierole(movie_id)
Range scan of composite used to satisfy where clause
Movie rows accessed by rowId to obtain Movie.id for join
(remaining steps are same as prior approach)
Range scan of foreign key index used to locate MovieRoles.movie_id
MovieRole rows accessed by rowId to satisfy select clause
* Eliminated much of the cost to locate children. Still have cost small cost obtaining Movie.id
Figure 80.10. Relative Test Result
FKs and Where Indexed.Get Children:warmups=2, rounds=11, ave=0.18 [+- 0.01]
Figure 80.11. DB Index(es)
create index movie_title_rdate_id_idx on jpatune_movie(title, release_date, id) create index movierole_movie_fkx on jpatune_movierole(movie_id)
Range scan of composite used to satisfy where clause and obtain Movie.id for join
No access to Movie table required
(remaining steps are same as prior approach)
Range scan of foreign key index used to locate MovieRoles.movie_id
MovieRole rows accessed by rowId to satisfy select clause
* The last rowId access is necessary to return the full MovieRole object
Figure 80.13. Relative Test Result
FKs, Where, and Join Indexed.Get Children:warmups=2, rounds=11, ave=0.20 [+- 0.01]
By adding the join column of the parent to the composite index used to locate the parent rows, you can save row accesses to the parent table (looking for the join column value). This is a small improvement relative to the other indexes added earlier because you are only eliminating an already efficient (but not the most efficient) means of locating row data.
Nested Loop Join - small driving table, join condition used to access second table
Hash Join - larger data joins based on equality matches through hashing functions
Sort-merge Join - larger data joins based on non-equality joins
Used when driving table is small and join condition used to access second table
Select all from table M joined with table P where P.cols match narrow condition
Figure 80.14. JPA Query
"select m from Movie m join m.cast as r where r.role=:role", params={role=Valentine McKee}, limit=2
Figure 80.15. Generated SQL
select * from ( select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ inner join JPATUNE_MOVIEROLE cast1_ on movie0_.ID=cast1_.MOVIE_ID where cast1_.MOVIE_ROLE=? ) where rownum <= ?
No indexes on where clause or foreign key columns
Full table scan of MovieRoles table performed to satisfy role from where clause
Nested Loop using driving table (MovieRole) foreign key (MovieRole.movie_id)
Unique Scan of Movie PK Index (SYS_C0012387) used to locate join rows (MovieRole.movie_id = Movie.id)
RowId scan used to access row data for Movie
Figure 80.17. Relative Test Result
No Table or FK Indexes.Small Driving Table:warmups=2, rounds=11, ave=0.28 [+- 0.04]
Most of the time spent within this query plan is attributed to the full table scan due to the lack of an index matching the where clause.
Single index on where clause column(s)
Range scan on index of MovieRoles.roles column used to satisfy where clause
MoveRoles row accessed using RowId from MoviesRoles.roles index to obtain movie_id
(from here is is the same as the previous full table scan case)
Nested Loop using driving table (MovieRole) foreign key (MovieRole.movie_id)
Unique Scan of Movie PK Index (SYS_C0012387) used to locate join rows (MovieRole.movie_id = Movie.id)
RowId scan used to access row data for Movie
Figure 80.20. Relative Test Result
Table Indexes Only.Small Driving Table:warmups=2, rounds=11, ave=0.18 [+- 0.03]
Adding the foreign key to an (composite) index will eliminate need to access row for foreign key during join.
Separate indexes; one for where clause and second for FK for join
Figure 80.21. DB Index(es)
create index movie_role_idx on jpatune_movierole(movie_role) create index movie_role_movie_fdx on jpatune_movierole(movie_id)
There is no difference between this and the previous case
Foreign Key indexes help locate child rows when join coming from parent
Index identifies rowId of the child having the parent value -- not the rowId of the parent
Figure 80.23. Relative Test Result
Individual Table and FK Indexes.Small Driving Table:warmups=2, rounds=11, ave=0.19 [+- 0.03]
A foreign key index (from the child to the parent) is not used when the child is the driving table and the parent is being located. In this case, the primary key index of the parent is used instead.
Single composite index containing columns from where clause and foreign key
Figure 80.24. DB Index(es)
create index movie_role_movie_cdx on jpatune_movierole(movie_role, movie_id)
Range scan on composite index of MovieRoles used to satisfy where clause
Composite index provides movie_id without having to access MovieRole row
(from here is is the same as the previous cases)
Nested Loop using driving table (MovieRole) foreign key (MovieRole.movie_id)
Unique Scan of Movie PK Index (SYS_C0012387) used to locate join rows (MovieRole.movie_id = Movie.id)
RowId scan used to access row data for Movie
Figure 80.26. Relative Test Result
Composite with FK Index.Small Driving Table:warmups=2, rounds=11, ave=0.19 [+- 0.03]
Adding the foreign key column to an index used to satisfy the where clause can eliminate the need for the DB to access the row for the foreign key during a join operation.
Used when driving table too large to fit into memory and join based on equality
Select all from table M joined with table P where P.cols match broad condition
Figure 80.27. JPA Query
-- Bounded "select m from Movie m join m.cast as r where r.role like :role", params={role=V%}, limit=2
-- Offset Bounded ... offset=1000, limit=2
-- Unbounded ... params={role=V%}
* Used multiple forms of query since Hash algorithm seemed to better detect an early completion limit than the nested loop approach.
Figure 80.28. Generated SQL
select * from ( select movie0_.ID as ID1_2_, movie0_.DIRECTOR_ID as DIRECTOR7_2_, movie0_.MINUTES as MINUTES2_2_, movie0_.PLOT as PLOT3_2_, movie0_.RATING as RATING4_2_, movie0_.RELEASE_DATE as RELEASE5_2_, movie0_.TITLE as TITLE6_2_ from JPATUNE_MOVIE movie0_ inner join JPATUNE_MOVIEROLE cast1_ on movie0_.ID=cast1_.MOVIE_ID where cast1_.MOVIE_ROLE like ? ) where rownum <= ?
No indexes on where clause or foreign key columns
Used When:
Joining large amounts of data
Joining on equality (equi-join) and not relative or function-based
Full table scan of MovieRoles table performed to satisfy role from where clause
DB creates in-memory hash table of foreign key values (i.e., multiple rows/FKs may hash into single hash entry)
Full table scan of Movie table performed to locate and hash(movie.id) -- primary key index is not used
Unique rows located within each hash bucket -- empties discarded
Matching Movies forwarded to next level
Figure 80.30. Relative Test Result
No Table or FK Indexes.Large Driving Table (Bounded):warmups=2, rounds=11, ave=0.17 [+- 0.01] No Table or FK Indexes.Large Driving Table Offset (Bounded):warmups=2, rounds=11, ave=0.26 [+- 0.02] No Table or FK Indexes.Large Driving Table (Unbounded:warmups=2, rounds=11, ave=31.51 [+- 1.25]
Single index on where clause column(s)
Range scan on index of MovieRoles.roles column used to satisfy where clause
MoveRoles row accessed using RowId from MoviesRoles.roles index to obtain movie_id
DB creates in-memory hash table of foreign key values
(from here is is the same as the previous full table scan case)
Full table scan of Movie table performed to locate and hash(movie.id) -- primary key index is not used
Unique rows located within each hash bucket -- empties discarded
Matching Movies forwarded to next level
Figure 80.33. Relative Test Result
Table Indexes Only.Large Driving Table (Bounded):warmups=2, rounds=11, ave=0.19 [+- 0.03] Table Indexes Only.Large Driving Table Offset (Bounded):warmups=2, rounds=11, ave=0.26 [+- 0.04] Table Indexes Only.Large Driving Table (Unbounded:warmups=2, rounds=11, ave=31.40 [+- 1.38]
Single composite index containing columns from where clause and foreign key
Figure 80.34. DB Index(es)
create index movie_role_movie_cdx on jpatune_movierole(movie_role, movie_id)
Range scan on composite index of MovieRoles used to satisfy where clause
Composite index provides movie_id without having to access MovieRole row
(from here is is the same as the previous cases)
Full table scan of Movie table performed to locate and hash(movie.id) -- primary key index is not used
Unique rows located within each hash bucket -- empties discarded
Matching Movies forwarded to next level
Figure 80.36. Relative Test Result
Composite with FK Index.Large Driving Table (Bounded):warmups=2, rounds=11, ave=0.18 [+- 0.01] Composite with FK Index.Large Driving Table Offset (Bounded):warmups=2, rounds=11, ave=0.22 [+- 0.03] Composite with FK Index.Large Driving Table (Unbounded:warmups=2, rounds=11, ave=30.20 [+- 1.24]
Used:
Over Nested Loops when no suitable driving table can be selected (i.e. fit in memory)
Over Hash Join when join is by non-equality comparison
Basic Algorithm
No driving table selected
Both tables ordered according to a common key
Ordered rows from both tables are then merged
Figure 80.37. Altered SQL
select * from ( select /*+ use_merge(cast1_ movie0_) */ movie0_.ID as ID1_2_, ... where cast1_.MOVIE_ROLE like ? ) where rownum <= ?
Results from MovieRole (movie_role=:role) and Movie (id=MovieRole.id) sorted at considerable cost
Sorted results merged to form result
Non-equijoins eliminate DB-flexibility to locate matching rows between tables and force the database to order tables based on the key column prior to forming joined result.
Avoiding inefficient uses of JPA.
Cost impacts of lazy and eager fetch joins
Get just the root parent of an object graph
Figure 81.1. DB Index(es)
alter table jpatune_moviegenre add constraint moviegenre_unique UNIQUE (movie_id, genre) create index movie_role_movie_fdx on jpatune_movierole(movie_id)
Figure 81.2. Service Tier Code
for (String id: movieIds) {
Movie m = dao.getMovieById(id);
m.getTitle();
}
Get just the root parent of an object graph with fetch=LAZY relationships
Figure 81.4. Relationships
public class Movie {
...
@ElementCollection(fetch=FetchType.LAZY)
@CollectionTable(
name="JPATUNE_MOVIEGENRE",
joinColumns=@JoinColumn(name="MOVIE_ID"),
uniqueConstraints=@UniqueConstraint(columnNames={"MOVIE_ID", "GENRE"}))
@Column(name="GENRE", length=20)
private Set<String> genres;
@ManyToOne(fetch=FetchType.LAZY,
cascade={CascadeType.PERSIST, CascadeType.DETACH})
@JoinColumn(name="DIRECTOR_ID")
private Director director;
@OneToMany(fetch=FetchType.LAZY,mappedBy="movie",
cascade={CascadeType.PERSIST, CascadeType.DETACH, CascadeType.REMOVE})
private Set<MovieRole> cast;
Two one-to-many relationships (cast and genres) -- genres is a non-entity ElementCollection
One one-to-one relationship (director)
All use fetch=LAZY
Figure 81.5. Generated SQL
select movie0_.ID as ID1_2_0_, movie0_.DIRECTOR_ID as DIRECTOR6_2_0_, movie0_.MINUTES as MINUTES2_2_0_, movie0_.RATING as RATING3_2_0_, movie0_.RELEASE_DATE as RELEASE4_2_0_, movie0_.TITLE as TITLE5_2_0_ from JPATUNE_MOVIE movie0_ where movie0_.ID=?
Only parent table is queried
Children are lazily loaded
Unique scan of Movie primary key index to satisfy where clause and locate rowId
Row access by rowId to satisfy select clause
Figure 81.7. Relative Test Result
- 20 movies Fetch Lazy Thin Parent.Get Parent:warmups=2, rounds=10, ave=0.32 [+- 0.03] Fetch Lazy Thick Parent.Get Parent:warmups=2, rounds=10, ave=0.58 [+- 0.03]
* "Thick" parent has an extra Movie.plot text property mapped to potentially make child joins more expensive. Thin parent does not have Movie plot mapped so the results can focus on access to smaller, related entities.
Choosing fetch=LAZY for relationships allows efficient access to the core information for that entity without paying a price for loading unnecessary relations.
Get just the root parent of an object graph with fetch=EAGER relationships
Figure 81.8. Relationships
<entity class="ejava.jpa.examples.tuning.bo.Movie">
<attributes>
<many-to-one name="director" fetch="EAGER">
<join-column name="DIRECTOR_ID"/>
</many-to-one>
<one-to-many name="cast" fetch="EAGER" mapped-by="movie"/>
<element-collection name="genres" fetch="EAGER">
<column name="GENRE"/>
<collection-table name="JPATUNE_MOVIEGENRE">
<join-column name="MOVIE_ID"/>
<unique-constraint>
<column-name>MOVIE_ID</column-name>
<column-name>GENRE</column-name>
</unique-constraint>
</collection-table>
</element-collection>
<transient name="plot"/>
</attributes>
</entity>
public class MovieRole {
...
@ManyToOne(optional=false, fetch=FetchType.LAZY,
cascade={CascadeType.DETACH})
@JoinColumn(name="ACTOR_ID")
private Actor actor;
<entity class="ejava.jpa.examples.tuning.bo.MovieRole">
<attributes>
<many-to-one name="actor" fetch="EAGER">
<join-column name="ACTOR_ID"/>
</many-to-one>
</attributes>
</entity>
public class Actor {
...
@OneToOne(optional=false, fetch=FetchType.EAGER,
cascade={CascadeType.PERSIST, CascadeType.DETACH})
@MapsId
@JoinColumn(name="PERSON_ID")
private Person person;
public class Director {
...
@OneToOne(optional=false, fetch=FetchType.EAGER,
cascade={CascadeType.PERSIST, CascadeType.DETACH})
@JoinColumn(name="PERSON_ID")
@MapsId
private Person person;
Movie.director, Movie.genres, Movie.cast relationships overridden in XML to be fetch=EAGER
MovieRole.actor relationship overridden in XML to be fetch=EAGER
Director.person and Actor.person already fetch=EAGER
Figure 81.9. Generated SQL
select movie0_.ID as ID1_2_5_, movie0_.DIRECTOR_ID as DIRECTOR6_2_5_, movie0_.MINUTES as MINUTES2_2_5_, movie0_.RATING as RATING3_2_5_, movie0_.RELEASE_DATE as RELEASE4_2_5_, movie0_.TITLE as TITLE5_2_5_, cast1_.MOVIE_ID as MOVIE4_2_7_, cast1_.ID as ID1_4_7_, cast1_.ID as ID1_4_0_, cast1_.ACTOR_ID as ACTOR3_4_0_, cast1_.MOVIE_ID as MOVIE4_4_0_, cast1_.MOVIE_ROLE as MOVIE2_4_0_, actor2_.PERSON_ID as PERSON1_0_1_, person3_.ID as ID1_5_2_, person3_.BIRTH_DATE as BIRTH2_5_2_, person3_.FIRST_NAME as FIRST3_5_2_, person3_.LAST_NAME as LAST4_5_2_, person3_.MOD_NAME as MOD5_5_2_, director4_.PERSON_ID as PERSON1_1_3_, person5_.ID as ID1_5_4_, person5_.BIRTH_DATE as BIRTH2_5_4_, person5_.FIRST_NAME as FIRST3_5_4_, person5_.LAST_NAME as LAST4_5_4_, person5_.MOD_NAME as MOD5_5_4_, genres6_.MOVIE_ID as MOVIE1_2_8_, genres6_.GENRE as GENRE2_3_8_ from JPATUNE_MOVIE movie0_ left outer join JPATUNE_MOVIEROLE cast1_ on movie0_.ID=cast1_.MOVIE_ID left outer join JPATUNE_ACTOR actor2_ on cast1_.ACTOR_ID=actor2_.PERSON_ID left outer join JPATUNE_PERSON person3_ on actor2_.PERSON_ID=person3_.ID left outer join JPATUNE_DIRECTOR director4_ on movie0_.DIRECTOR_ID=director4_.PERSON_ID left outer join JPATUNE_PERSON person5_ on director4_.PERSON_ID=person5_.ID left outer join JPATUNE_MOVIEGENRE genres6_ on movie0_.ID=genres6_.MOVIE_ID where movie0_.ID=?
Single query (as before), but this time it also brings in the entire object graph
Explain plan shows use of indexes and no full table scans (a missing FK index for MovieRole.movie could have been costly)
Execution plan is significantly more costly than lazy alternative
Figure 81.11. Relative Test Result
- 20 movies Fetch Eager Thin Parent.Get Parent:warmups=2, rounds=10, ave=2.43 [+- 0.47] Fetch Eager Thick Parent.Get Parent:warmups=2, rounds=10, ave=2.59 [+- 0.06]
Choosing fetch=EAGER for relationships will make access to a single entity within the relationship more expensive to access. Consider creating a value query, value query with a result class, or a native query when querying for only single entity under these conditions.
Get just the root parent and *ONLY* the parent of an object graph with fetch=EAGER relationships by using a NativeQuery and Result Class
Figure 81.13. DAO Code
public Movie getMovieByIdUnfetched(String id) {
List<Movie> movies=createQuery(
String.format("select new %s(m.id, m.minutes, m.rating, m.releaseDate, m.title) ", Movie.class.getName()) +
"from Movie m " +
"where id=:id", Movie.class)
.setParameter("id", id)
.getResultList();
return movies.isEmpty() ? null : movies.get(0);
}
public Movie(String id, Integer minutes, String rating, Date releaseDate, String title) {
this.id=id;
this.minutes = minutes;
this.rating=rating;
this.mrating=MovieRating.getFromMpaa(rating);
this.releaseDate = releaseDate;
this.title=title;
}
We define an alternate JPA-QL query for provider to use that does not include relationships
Result class not mapped beyond this query -- does not have to be an entity class
Figure 81.14. Generated JPAQL and SQL
"select new ejava.jpa.examples.tuning.bo.Movie(m.id, m.minutes, m.rating, m.releaseDate, m.title) from Movie m where id=:id", params={id=m270003}
select movie0_.ID as col_0_0_, movie0_.MINUTES as col_1_0_, movie0_.RATING as col_2_0_, movie0_.RELEASE_DATE as col_3_0_, movie0_.TITLE as col_4_0_ from JPATUNE_MOVIE movie0_ where movie0_.ID=?
Value query with result class constructor permits a bypass of fetch=EAGER relationships
Execution plan identical to fetch=LAZY case
Figure 81.16. Relative Test Result
Fetch Eager Thin Parent.Get Just Parent:warmups=2, rounds=10, ave=0.32 [+- 0.02] Fetch Eager Thick Parent.Get Just Parent:warmups=2, rounds=10, ave=0.32 [+- 0.02] Fetch Lazy Thin Parent.Get Just Parent:warmups=2, rounds=10, ave=0.34 [+- 0.03] Fetch Lazy Thick Parent.Get Just Parent:warmups=2, rounds=10, ave=0.32 [+- 0.02]
* Thick/Thin or Eager/Lazy should not matter to this approach since we only access a specific set of fields in the Movie that does not include Movie.plot.
JPA provides many escape hatches to achieve desired results. Use JPAQL value queries with Object[], Tuple, or custom result class -or- use SQL to provide efficient override of fetch=EAGER definitions.
Even though we used an entity class in this example, it is being used as a simple POJO and the returned instance is not managed. Result Classes for value queries provide convenient type-safe access to results. However, they are unmanaged and cannot be used for follow-on entity operations.
Get parent and children in an object graph
Figure 81.17. DB Index(es)
alter table jpatune_moviegenre add constraint moviegenre_unique UNIQUE (movie_id, genre) create index movie_role_movie_fdx on jpatune_movierole(movie_id)
Figure 81.18. Service Tier Code
for (String id: movieIds) {
Movie m = dao.getMovieById(id);
if (m.getDirector()!=null) { m.getDirector().getPerson().getLastName(); }
m.getGenres().iterator().next();
for (MovieRole role: m.getCast()) {
if (role.getActor()!=null) { role.getActor().getPerson().getLastName(); }
}
}
Get parent and children in an object graph with fetch=LAZY relationships
Query for parent
select movie0_.ID as ID1_2_0_, movie0_.DIRECTOR_ID as DIRECTOR6_2_0_, movie0_.MINUTES as MINUTES2_2_0_, movie0_.RATING as RATING3_2_0_, movie0_.RELEASE_DATE as RELEASE4_2_0_, movie0_.TITLE as TITLE5_2_0_ from JPATUNE_MOVIE movie0_ where movie0_.ID=?
Query for Movie.director role
select director0_.PERSON_ID as PERSON1_1_0_ from JPATUNE_DIRECTOR director0_ where director0_.PERSON_ID=?;
Query for Movie.director.person identity
select person0_.ID as ID1_5_0_, person0_.BIRTH_DATE as BIRTH2_5_0_, person0_.FIRST_NAME as FIRST3_5_0_, person0_.LAST_NAME as LAST4_5_0_, person0_.MOD_NAME as MOD5_5_0_ from JPATUNE_PERSON person0_ where person0_.ID=?;
Query for Movie.genre values
select genres0_.MOVIE_ID as MOVIE1_2_0_, genres0_.GENRE as GENRE2_3_0_ from JPATUNE_MOVIEGENRE genres0_ where genres0_.MOVIE_ID=?;
Query for Movie.cast entities
select cast0_.MOVIE_ID as MOVIE4_2_1_, cast0_.ID as ID1_4_1_, cast0_.ID as ID1_4_0_, cast0_.ACTOR_ID as ACTOR3_4_0_, cast0_.MOVIE_ID as MOVIE4_4_0_, cast0_.MOVIE_ROLE as MOVIE2_4_0_ from JPATUNE_MOVIEROLE cast0_ where cast0_.MOVIE_ID=?;
Query for MovieRole.actor entity
select actor0_.PERSON_ID as PERSON1_0_0_ from JPATUNE_ACTOR actor0_ where actor0_.PERSON_ID=? ... --repeated for each member of the Movie.cast
Query for Actor.person entity
select person0_.ID as ID1_5_0_, person0_.BIRTH_DATE as BIRTH2_5_0_, person0_.FIRST_NAME as FIRST3_5_0_, person0_.LAST_NAME as LAST4_5_0_, person0_.MOD_NAME as MOD5_5_0_ from JPATUNE_PERSON person0_ where person0_.ID=?; ... --repeated for each member of the Movie.cast
Fetch Lazy Thin Parent.Get Parent and Children:warmups=2, rounds=10, ave=7.25 [+- 0.91] Fetch Lazy Thick Parent.Get Parent and Children:warmups=2, rounds=10, ave=7.25 [+- 0.92]
Fetch=LAZY is intended for access to a minimal set of objects within a graph. Although each individual query by primary key is quite inexpensive, the sum total (factoring in cardinality) is significant. Consider adding a JOIN FETCH query to your DAO when accessing entire object graph declared with fetch=LAZY.
Get parent and children in an object graph with fetch=EAGER relationships
Loading of the object graph occurs in single DB call
Same cost loading object graph -- whether accessing single or all entities
Figure 81.29. Relative Test Result
Fetch Eager Thin Parent.Get Parent and Children:warmups=2, rounds=10, ave=2.26 [+- 0.06] Fetch Eager Thick Parent.Get Parent and Children:warmups=2, rounds=10, ave=2.57 [+- 0.06]
Use fetch=EAGER if related entity *always*/*mostly* accessed. Suggest using a JOIN FETCH query when fetch=LAZY is defined and need entire object graph.
Get parent and children in an object graph with JOIN FETCH query
Figure 81.31. DAO Code/JPAQL
public Movie getMovieFetchedByIdFetched(String id) {
List<Movie> movies = createQuery(
"select m from Movie m " +
"left join fetch m.genres " +
"left join fetch m.director d " +
"left join fetch d.person " +
"left join fetch m.cast role " +
"left join fetch role.actor a " +
"left join fetch a.person " +
"where m.id=:id", Movie.class)
.setParameter("id", id)
.getResultList();
return movies.isEmpty() ? null : movies.get(0);
}
EAGER aspects adding by query adding FETCH to JOIN query construct
Figure 81.32. Generated SQL
select movie0_.ID as ID1_2_0_, director2_.PERSON_ID as PERSON1_1_1_, person3_.ID as ID1_5_2_, cast4_.ID as ID1_4_3_, actor5_.PERSON_ID as PERSON1_0_4_, person6_.ID as ID1_5_5_, movie0_.DIRECTOR_ID as DIRECTOR6_2_0_, movie0_.MINUTES as MINUTES2_2_0_, movie0_.RATING as RATING3_2_0_, movie0_.RELEASE_DATE as RELEASE4_2_0_, movie0_.TITLE as TITLE5_2_0_, genres1_.MOVIE_ID as MOVIE1_2_0__, genres1_.GENRE as GENRE2_3_0__, person3_.BIRTH_DATE as BIRTH2_5_2_, person3_.FIRST_NAME as FIRST3_5_2_, person3_.LAST_NAME as LAST4_5_2_, person3_.MOD_NAME as MOD5_5_2_, cast4_.ACTOR_ID as ACTOR3_4_3_, cast4_.MOVIE_ID as MOVIE4_4_3_, cast4_.MOVIE_ROLE as MOVIE2_4_3_, cast4_.MOVIE_ID as MOVIE4_2_1__, cast4_.ID as ID1_4_1__, person6_.BIRTH_DATE as BIRTH2_5_5_, person6_.FIRST_NAME as FIRST3_5_5_, person6_.LAST_NAME as LAST4_5_5_, person6_.MOD_NAME as MOD5_5_5_ from JPATUNE_MOVIE movie0_ left outer join JPATUNE_MOVIEGENRE genres1_ on movie0_.ID=genres1_.MOVIE_ID left outer join JPATUNE_DIRECTOR director2_ on movie0_.DIRECTOR_ID=director2_.PERSON_ID left outer join JPATUNE_PERSON person3_ on director2_.PERSON_ID=person3_.ID left outer join JPATUNE_MOVIEROLE cast4_ on movie0_.ID=cast4_.MOVIE_ID left outer join JPATUNE_ACTOR actor5_ on cast4_.ACTOR_ID=actor5_.PERSON_ID left outer join JPATUNE_PERSON person6_ on actor5_.PERSON_ID=person6_.ID where movie0_.ID=?;
Query identical to fetch=EAGER case
Figure 81.34. Relative Test Result
Fetch Lazy Thin Parent.Get Parent with Fetched Children:warmups=2, rounds=10, ave=2.56 [+- 0.80] Fetch Lazy Thick Parent.Get Parent with Fetched Children:warmups=2, rounds=10, ave=2.54 [+- 0.08] Fetch Eager Thin Parent.Get Parent with Fetched Children:warmups=2, rounds=10, ave=2.24 [+- 0.06] Fetch Eager Thick Parent.Get Parent with Fetched Children:warmups=2, rounds=10, ave=2.58 [+- 0.05]
JPA provides several means to access an entity and its relationships. The properties defined for the relationship help define the default query semantics -- which may not be appropriate for all uses. Leverage JOIN FETCH queries when needing to fully load specific relationships prior to ending the transaction.
Pulling back entire rows from table rather than just the count
Figure 81.35. DB Index(es)
alter table jpatune_moviegenre add constraint moviegenre_unique UNIQUE (movie_id, genre) create index movie_role_movie_fdx on jpatune_movierole(movie_id)
Get size of a relationship collection.
Figure 81.37. DAO Code/JPAQL
public int getMovieCastCountByDAORelation(String movieId) {
Movie m = em.find(Movie.class, movieId);
return m==null ? 0 : m.getCast().size();
}
DAO first gets Movie by primary key
DAO follows up by accessing size of relationship
Figure 81.38. Generated SQL - Fetch=LAZY
select movie0_.ID as ID1_2_0_, movie0_.DIRECTOR_ID as DIRECTOR6_2_0_, movie0_.MINUTES as MINUTES2_2_0_, movie0_.RATING as RATING3_2_0_, movie0_.RELEASE_DATE as RELEASE4_2_0_, movie0_.TITLE as TITLE5_2_0_ from JPATUNE_MOVIE movie0_ where movie0_.ID=?
select cast0_.MOVIE_ID as MOVIE4_2_1_, cast0_.ID as ID1_4_1_, cast0_.ID as ID1_4_0_, cast0_.ACTOR_ID as ACTOR3_4_0_, cast0_.MOVIE_ID as MOVIE4_4_0_, cast0_.MOVIE_ROLE as MOVIE2_4_0_ from JPATUNE_MOVIEROLE cast0_ where cast0_.MOVIE_ID=?
Provider will issue follow-on query for all entities in relation
Entity data not used by DAO -- i.e. wasted
Provider will issue one query for entire object graph, to include entities in relation
Entity data not used by DAO -- i.e. even more data is wasted
Figure 81.40. Relative Test Result
Size Lazy.DAO Relation Count:warmups=2, rounds=10, ave=1.83 [+- 0.82] Size Eager.DAO Relation Count:warmups=2, rounds=10, ave=5.40 [+- 0.83]
Pulling back relationship graphs to just obtain the count in that relationship is inefficient and requires DB to do much more work than it has to.
Query for relationships and count resulting rows
Figure 81.41. DAO Code/JPAQL
public int getMovieCastCountByDAO(String movieId) {
return createQuery(
"select role " +
"from Movie m " +
"join m.cast role " +
"where m.id=:id", MovieRole.class)
.setParameter("id", movieId)
.getResultList().size();
}
"select role from Movie m join m.cast role where m.id=:id", params={id=m48306}
DAO forms query for just related entities
Figure 81.42. Generated SQL - Fetch=LAZY
select cast1_.ID as ID1_4_, cast1_.ACTOR_ID as ACTOR3_4_, cast1_.MOVIE_ID as MOVIE4_4_, cast1_.MOVIE_ROLE as MOVIE2_4_ from JPATUNE_MOVIE movie0_ inner join JPATUNE_MOVIEROLE cast1_ on movie0_.ID=cast1_.MOVIE_ID where movie0_.ID=?
Provider forms query for related entities
Figure 81.43. Generated SQL - Fetch=EAGER
select cast1_.ID as ID1_4_, cast1_.ACTOR_ID as ACTOR3_4_, cast1_.MOVIE_ID as MOVIE4_4_, cast1_.MOVIE_ROLE as MOVIE2_4_ from JPATUNE_MOVIE movie0_ inner join JPATUNE_MOVIEROLE cast1_ on movie0_.ID=cast1_.MOVIE_ID where movie0_.ID=?
--this is repeated for each MovieRole.actor select actor0_.PERSON_ID as PERSON1_0_0_ from JPATUNE_ACTOR actor0_ where actor0_.PERSON_ID=? select person0_.ID as ID1_5_0_, person0_.BIRTH_DATE as BIRTH2_5_0_, person0_.FIRST_NAME as FIRST3_5_0_, person0_.LAST_NAME as LAST4_5_0_, person0_.MOD_NAME as MOD5_5_0_ from JPATUNE_PERSON person0_ where person0_.ID=?
Provider forms query for related entities and fetch=EAGER relationship specification causes additional MovieRole.actor to be immediately obtained -- except through separate queries by primary key.
Figure 81.44. Relative Test Result
Size Lazy.DAO Count:warmups=2, rounds=10, ave=1.37 [+- 0.03] Size Eager.DAO Count:warmups=2, rounds=10, ave=16.82 [+- 1.33]
When the entity model over-uses fetch=EAGER, the negative results can be magnified when pulling back unnecessary information from the database.
Issue query to DB to return relationship count
Figure 81.45. DAO Code/JPAQL
public int getMovieCastCountByDB(String movieId) {
return createQuery(
"select count(role) " +
"from Movie m " +
"join m.cast role " +
"where m.id=:id", Number.class)
.setParameter("id", movieId)
.getSingleResult().intValue();
}
"select count(role) from Movie m join m.cast role where m.id=:id", params={id=m48306}
Requesting count(role) rather than returning all roles and counting in DAO.
Figure 81.46. Generated SQL
select count(cast1_.ID) as col_0_0_ from JPATUNE_MOVIE movie0_ inner join JPATUNE_MOVIEROLE cast1_ on movie0_.ID=cast1_.MOVIE_ID where movie0_.ID=?
Produces a single result/row
DB has already optimized the query to remove unnecessary JOIN
Figure 81.48. Relative Test Result
Size Lazy.DB Count:warmups=2, rounds=10, ave=0.50 [+- 0.04] Size Eager.DB Count:warmups=2, rounds=10, ave=0.45 [+- 0.02]
* fetch=LAZY or EAGER has no impact in this solution
It is much more efficient to perform counts within database than to count rows from data returned.
Restructure query to DB to count relations without JOIN
Figure 81.49. DAO Code/JPAQL
public int getCastCountForMovie(String movieId) {
return createQuery(
"select count(*) " +
"from MovieRole role " +
"where role.movie.id=:id", Number.class)
.setParameter("id", movieId)
.getSingleResult().intValue();
}
"select count(*) from MovieRole role where role.movie.id=:id", params={id=m48306}
Querying from MovieRole side allows removal of unnecessary JOIN
Figure 81.50. Generated SQL
select count(*) as col_0_0_ from JPATUNE_MOVIEROLE movierole0_ where movierole0_.MOVIE_ID=?
SQL generated queries single table
No improvement in execution plan since DB already performed this optimization in our last attempt
Figure 81.52. Relative Test Result
Size Lazy.DB Count no Join:warmups=2, rounds=10, ave=0.47 [+- 0.01] Size Eager.DB Count no Join:warmups=2, rounds=10, ave=0.44 [+- 0.02]
* fetch=LAZY or EAGER has no impact in this solution
Although in this and the previous case the DB looks to have already optimized the query, try to eliminate unnecessary complexity within the query.
Performing separate subqueries off initial query
Get people who have acted in same movie as a specific actor
Figure 81.53. DB Index(es)
create index movierole_actor_movie_cdx on jpatune_movierole(actor_id, movie_id) create index movierole_movie_actor_cdx on jpatune_movierole(movie_id, actor_id)
Using DAO to perform initial query and follow-on queries based on results
Figure 81.55. DAO Code/JPAQL
public Collection<Person> oneStepFromPersonByDAO(Person p) {
Collection<Person> result = new HashSet<Person>();
//performing core query
List<String> movieIds = createQuery(
"select role.movie.id from MovieRole role " +
"where role.actor.person.id=:personId", String.class)
.setParameter("personId", p.getId())
.getResultList();
//loop through results and issue sub-queries
for (String mid: movieIds) {
List<Person> people = createQuery(
"select role.actor.person from MovieRole role " +
"where role.movie.id=:movieId", Person.class)
.setParameter("movieId", mid)
.getResultList();
result.addAll(people);
}
return result;
}
"select role.movie.id from MovieRole role where role.actor.person.id=:personId", params={personId=p73897}
//repeated for each returned movie above "select role.actor.person from MovieRole role where role.movie.id=:movieId", params={movieId=m239584}
DAO makes N separate queries based on results of first query
Distinct was not required in query since it was issued per-movie. Distinct was handed within the DAO using a Java Set to hold the results and hashcode()/equals() implemented within Person class.
Figure 81.56. Generated SQL
select movierole0_.MOVIE_ID as col_0_0_ from JPATUNE_MOVIEROLE movierole0_, JPATUNE_ACTOR actor1_ where movierole0_.ACTOR_ID=actor1_.PERSON_ID and actor1_.PERSON_ID=?
--repeated for each returned movie above select person2_.ID as ID1_5_, person2_.BIRTH_DATE as BIRTH2_5_, person2_.FIRST_NAME as FIRST3_5_, person2_.LAST_NAME as LAST4_5_, person2_.MOD_NAME as MOD5_5_ from JPATUNE_MOVIEROLE movierole0_, JPATUNE_ACTOR actor1_ inner join JPATUNE_PERSON person2_ on actor1_.PERSON_ID=person2_.ID where movierole0_.ACTOR_ID=actor1_.PERSON_ID and movierole0_.MOVIE_ID=?
SQL generated queries single table
Range scan of compound index(actor_id, person_id) used to satisfy the person_id in where clause and movie_id for select clause
* Query was optimized to bypass Actor table
Range scan on compound_index(movie_id, actor_id) to satisfy where condition for Movie.id and locate MovieRole rowId
Unique scan of Person primary key index to obtain rowId for Person.id=MovieRole.actor.id
Person row accessed by rowId to satisfy select clause
* Query was optimized to bypass Actor table
** Compound index(movie_id, actor_id) permitted MovieRole table and lookup of MovieRole.actorId to be bypassed.
Figure 81.59. Relative Test Result
-3924 people found Loop Lazy.DAO Loop:warmups=2, rounds=10, ave=4.91 [+- 0.88]
Query loops driven off of previous query results can be a sign the follow-queries could have been combined with the initial query to be accomplished within the database within a single statement.
Also beware of how the loop size can grow over the lifetime of the application. The number may grow significantly after testing/initial deployment to a significant size (i.e., from 100 to 100K loops). Defining as a single statement and applying paging limits can help speed the originally flawed implementation.
Expressing nest query as subquery to resolve within database.
Figure 81.60. DAO Code/JPAQL
public List<Person> oneStepFromPersonByDB(Person p) {
return createQuery(
"select distinct role.actor.person from MovieRole role " +
"where role.movie.id in (" +
"select m.id from Movie m " +
"join m.cast role2 " +
"where role2.actor.person.id=:id)", Person.class)
.setParameter("id", p.getId())
.getResultList();
}
"select distinct role.actor.person from MovieRole role where role.movie.id in ( select m.id from Movie m join m.cast role2 where role2.actor.person.id=:id )", params={id=p73897}
Distinct was required to remove duplicates
Figure 81.61. Generated SQL
select distinct person2_.ID as ID1_5_, person2_.BIRTH_DATE as BIRTH2_5_, person2_.FIRST_NAME as FIRST3_5_, person2_.LAST_NAME as LAST4_5_, person2_.MOD_NAME as MOD5_5_ from JPATUNE_MOVIEROLE movierole0_, JPATUNE_ACTOR actor1_ inner join JPATUNE_PERSON person2_ on actor1_.PERSON_ID=person2_.ID where movierole0_.ACTOR_ID=actor1_.PERSON_ID and ( movierole0_.MOVIE_ID in ( select movie3_.ID from JPATUNE_MOVIE movie3_ inner join JPATUNE_MOVIEROLE cast4_ on movie3_.ID=cast4_.MOVIE_ID, JPATUNE_ACTOR actor5_ where cast4_.ACTOR_ID=actor5_.PERSON_ID and actor5_.PERSON_ID=? ) )
SQL generated uses a non-correlated subquery
Range scan of MovieRole composite(actor_id, movie_id) used to satisfy subquery where clause for person_id and obtain movie_id
Range scan of MovieRole composite(movie_id, actor_id) used to join movie_ids with root query and obtain person_id
Unique scan of Person primary key index to locate rowId
Person row accessed by rowId
* cast4_ and movierole0_ are both MovieRoles. cast4_ is joined with Movie and subject Person in subquery. movierole0_ is joined with Movie and associated Person in root query.
** execution plan indicates subquery could have been re-written as a JOIN
Figure 81.63. Relative Test Result
-3924 people found Loop Lazy.DB Loop Distinct:warmups=2, rounds=10, ave=2.52 [+- 0.09]
If possible, express query as a single transaction to the database rather than pulling data back and re-issuing subqueries from the DAO. Apply paging constraints in order to address issues with excess growth over time.
Placing reasonable limits on amount of data returned
Figure 81.64. DB Index(es)
alter table jpatune_moviegenre add constraint moviegenre_unique UNIQUE (movie_id, genre) create index movie_role_movie_fdx on jpatune_movierole(movie_id)
Figure 81.65. Service Tier Code
dao.oneStepFromPerson(kevinBacon, offset, PAGE_SIZE, "role.actor.person.lastName ASC")
* Service tier provides not only offset and limit information, but would also benefit from using a sort. The DAO would have to pull back all rows in order to implement the sort within the DAO -- while the DB-based solution can leave all data within the database. Sort will only be implemented here in the DB-based solution.
Implementing paging within the DAO
Figure 81.66. DAO Code/JPAQL
public Collection<Person> oneStepFromPersonByDAO(Person p, Integer offset, Integer limit, String orderBy) {
Collection<Person> result = new HashSet<Person>();
//performing core query
List<String> movieIds = createQuery(
"select role.movie.id from MovieRole role " +
"where role.actor.person.id=:personId", String.class)
.setParameter("personId", p.getId())
.getResultList();
//loop through results and issue sub-queries
int pos=0;
for (String mid: movieIds) {
List<Person> people = createQuery(
"select role.actor.person from MovieRole role " +
"where role.movie.id=:movieId", Person.class)
.setParameter("movieId", mid)
.getResultList();
if (offset==null || (offset!=null && pos+people.size() > offset)) {
for(Person pp: people) {
if (offset==null || pos>=offset) {
result.add(pp);
if (limit!=null && result.size() >= limit) { break; }
}
pos+=1;
}
} else {
pos+=people.size();
}
if (limit!=null && result.size() >= limit) { break; }
}
return result;
}
DAO logic gets complicated when self-implementing paging.
OrderBy is not implemented by this DAO algorithm. All rows in the table would be required in order to perform ordering within the DAO.
Figure 81.69. Relative Test Result
Loop Lazy.DAO Page 0:warmups=1, rounds=10, ave=0.39 [+- 0.09] Loop Lazy.DAO Page 10:warmups=1, rounds=10, ave=1.01 [+- 0.45] Loop Lazy.DAO Page 50:warmups=1, rounds=10, ave=2.80 [+- 0.79] Loop Eager.DAO Page 0:warmups=1, rounds=10, ave=0.38 [+- 0.10] Loop Eager.DAO Page 10:warmups=1, rounds=10, ave=1.10 [+- 0.64] Loop Eager.DAO Page 50:warmups=1, rounds=10, ave=2.53 [+- 0.06]
This example performs better or equal to previous loop/subquery example when earlier pages are requested -- thus fewer rows returned
fetch=EAGER/LAZY have no impact on this test since the entity queried for has no relationships
Paging within the Java code starts off near equivalent to the database solution for small row sizes but gradually gets worse when row sizes increase and later pages are requested.
Implementing paging within the database
Figure 81.70. DAO Code/JPAQL
public List<Person> oneStepFromPersonByDB(Person p, Integer offset, Integer limit, String orderBy) {
return withPaging(createQuery(
"select distinct role.actor.person from MovieRole role " +
"where role.movie.id in (" +
"select m.id from Movie m " +
"join m.cast role2 " +
"where role2.actor.person.id=:id)", Person.class), offset, limit, orderBy)
.setParameter("id", p.getId())
.getResultList();
}
"select distinct role.actor.person from MovieRole role where role.movie.id in ( select m.id from Movie m join m.cast role2 where role2.actor.person.id=:id ) order by role.actor.person.lastName ASC", params={id=p73897}, offset=500, limit=50
JPA calls to TypedQuery.setFirstResult(), setMaxResults(), and adding the orderBy to the text of the query are abstracted behind withQuery() and createQuery() helper functions within the DAO example class and are not shown here
Figure 81.71. Generated SQL
select * from ( select row_.*, rownum rownum_ from ( select distinct person2_.ID as ID1_5_, person2_.BIRTH_DATE as BIRTH2_5_, person2_.FIRST_NAME as FIRST3_5_, person2_.LAST_NAME as LAST4_5_, person2_.MOD_NAME as MOD5_5_ from JPATUNE_MOVIEROLE movierole0_, JPATUNE_ACTOR actor1_ inner join JPATUNE_PERSON person2_ on actor1_.PERSON_ID=person2_.ID, JPATUNE_PERSON person7_ where movierole0_.ACTOR_ID=actor1_.PERSON_ID and actor1_.PERSON_ID=person7_.ID and ( movierole0_.MOVIE_ID in ( select movie3_.ID from JPATUNE_MOVIE movie3_ inner join JPATUNE_MOVIEROLE cast4_ on movie3_.ID=cast4_.MOVIE_ID, JPATUNE_ACTOR actor5_ where cast4_.ACTOR_ID=actor5_.PERSON_ID and actor5_.PERSON_ID=? ) ) order by person7_.LAST_NAME ASC ) row_ where rownum <= ?) where rownum_ > ?
Generated SQL is essentially the same as the looping/subquery example except with the addition of "order by" and paging constructs.
Execution plan is similar as described in the looping/subquery example
Extra cost to sort rows prior to paging operation added
Figure 81.73. Relative Test Result
Loop Lazy.DB Page 0:warmups=1, rounds=10, ave=0.32 [+- 0.02] Loop Lazy.DB Page 10:warmups=1, rounds=10, ave=0.31 [+- 0.01] Loop Lazy.DB Page 50:warmups=1, rounds=10, ave=0.32 [+- 0.01] Loop Eager.DB Page 0:warmups=1, rounds=10, ave=0.32 [+- 0.01] Loop Eager.DB Page 10:warmups=1, rounds=10, ave=0.32 [+- 0.01] Loop Eager.DB Page 50:warmups=1, rounds=10, ave=0.32 [+- 0.00]
Since only the requested page of rows is returned, delegating paging to the database provides consistent performance for varying row quantities.
Implementing paging across multiple subquery calls to the database can get ugly and error-prone. Implementing paging for a single query issued to the database is quite trivial.
Hidden performance costs for fetch and resolving with JOIN FETCH and value queries
Hidden performance costs for row counts and resolving with a query function
Performance costs when issuing repeated queries of an initial query from DAO and resolving using subqueries in database
Performance and functional costs when implementing paging in DAO and resolving by delegating paging to database
Figure 82.1. Explain Plan: Where Column Not Indexed
EXPLAIN select m.title, role.movie_role from queryex_movie m join queryex_movierole role on role.movie_id = m.id where m.rating='R' and role.movie_role='Chip Diller';
PLAN SELECT M.TITLE, ROLE.MOVIE_ROLE FROM PUBLIC.QUERYEX_MOVIEROLE ROLE /* PUBLIC.QUERYEX_MOVIEROLE.tableScan */ /* WHERE ROLE.MOVIE_ROLE = 'Chip Diller' */ INNER JOIN PUBLIC.QUERYEX_MOVIE M /* PUBLIC.PRIMARY_KEY_C7B: ID = ROLE.MOVIE_ID */ ON 1=1 WHERE (ROLE.MOVIE_ID = M.ID) AND ((M.RATING = 'R') AND (ROLE.MOVIE_ROLE = 'Chip Diller'))
Figure 82.2. Index Where Column
create index movierole_role_movie_idx on queryex_movierole(movie_role)
Figure 82.3. Explain Plan: Where Column Indexed
PLAN SELECT M.TITLE, ROLE.MOVIE_ROLE FROM PUBLIC.QUERYEX_MOVIEROLE ROLE /* PUBLIC.MOVIEROLE_ROLE_MOVIE_IDX: MOVIE_ROLE = 'Chip Diller' */ /* WHERE ROLE.MOVIE_ROLE = 'Chip Diller' */ INNER JOIN PUBLIC.QUERYEX_MOVIE M /* PUBLIC.PRIMARY_KEY_C7B: ID = ROLE.MOVIE_ID */ ON 1=1 WHERE (ROLE.MOVIE_ID = M.ID) AND ((M.RATING = 'R') AND (ROLE.MOVIE_ROLE = 'Chip Diller'))
Some topics of possible interest that we did not cover.
Dynamic SQL - parsing penalties paid by building complete SQL strings rather than using a prepared statement template and value parameters.
Cluster Indexes - structurally organize parent and child tables within same data block when commonly accessed together through join operations and rarely alone.
Concurrency - sessions blocked inadvertently by concurrent sessions
...
Table of Contents
Introduce the purpose of an EJB (why?)
Introduce the different EJB types
Introduce different EJB interface options
Introduce different deployment options for EJBs
At the completion of this topic, the student shall be able to:
State reasons for/against adding an EJB to a JavaEE application
State the four different types of EJBs and reasons for/against using each
State the three different built-in EJB interface types and when to/not-to use each
State the three different deployment options and when to/not-to use each
Web integration adds an additional HTTP/REST-capable interface to EJB.
Potential tight coupling between clients and complex business objects
Too many method invocations between clients and business objects
Business methods exposed for misuse by clients
Hide complex interactions behind a simple client interface
Reduce the number of business objects exposed to the client across the network
Hide implementation, interactions, and dependencies of business components
Use a business logic class to encapsulate the interactions required with the business objects
Simplifies complex systems
May appear to be a no value pass-thru in simple systems
Should involve more than one business object per facade
Should have more than one facade per system
Decouples the business objects from being aware of one another
Improves perceived network performance for remote interfaces
Centralizes security and transactions in some cases
You have already implemented the Session Facade Pattern with your POJO business logic from previous work. There is nothing magical about server-side EJBs over POJO business logic, relative to the Session Facade Pattern, except for the enhanced ability to implement remote, transactional, secure, and load-balanced behavior without impacting the focus on the business.
Remote clients require access to server-side resources.
Core service layer (business logic and its business objects) contains fine grain objects and methods
Significant number of fine-grain remote calls will not work
Provide a Remote Facade
Coarse-grain facade over service layer
Contains no detailed business logic -- it calls it
Translates course-grain methods and objects into fine-grain method calls and objects to/from service layer
Bulk accessors wrap fine-grain access methods
Service Layer does not have a remote interface
Fine-grain business objects mapped to database might not be serializable
Business Objects represent too much information or behavior to transfer to remote client
Client may get information they don't need
Client may get information they can't handle
Client may get information they are not authorized to use
Client may get too much information/behavior to be useful (e.g., entire database serialized to client)
Some clients are local and can share object references with business logic
Handling specifics of remote clients outside of core scope of business logic
Layer a Remote Facade over Business Logic
Remote Facade constructs Data Transfer Objects (DTOs) from Business Objects that are appropriate for remote client view
Remote Facade uses DTOs to construct or locate Business Objects to communicate with Business Logic
Clients only get what they need
Clients only get what they understand
Clients only get what they are authorized to use
Remote and Local interfaces to services are different
Makes it harder to provide location transparency
Lightweight Business Objects can be used as DTOs
Remote Facade must make sure they are “pruned” of excess related items before transferring to client
Remote Facade must make sure they are “cleaned” of DAO persistence classes before transferring to client
Implement a task
Interact with other beans
Examples:
TaxDistrict.calcTax(double amount)
Teller.transfer(long fromAccount, long toAccount, double amount)
Registrar.listStudents(String course, int offset, int limit)
Session EJB per Use Case too fine
CreateAccountEJB
DepositEJB
WithdrawEJB
TransferEJB
Group cohesive Use Cases into larger-grain Session EJBs
Teller
createAccount()
deposit()
withdraw()
transfer()
All are POJO-based
i.e., most Java classes can be made into an EJB
Figure 85.1. Example Entity
@javax.persistence.Entity
@javax.persistence.Queries({
@javax.persistence.Query(name="Reservation.getReservationsByName", query="
select r from Reservation r
where r.person.firstName like :firstName and
r.person.lastName like :lastName")
})
public class Reservation {
@javax.persistence.Id
private long id;
private String confirmation;
@javax.persistence.Temporal(javax.persistence.TemporalType.DATE)
private Date startDate;
@javax.persistence.Temporal(javax.persistence.TemporalType.DATE)
private Date endDate;
@javax.persistence.ManyToOne
@javax.persistence.JoinColumn
private Person person;
Represents shared data in the database
Typically interact at row/object level
Moved to Java Persistence API (JPA) Spec in EJB 3.0/JavaEE 5
Not constrained to server-side
Examples:
Account, Owner
Account.deposit(double amount)
Account.getOwner()
Good for representing persistent information
Contains no means to access external resources other than directly related information
Figure 85.3. Example Stateless EJB
@javax.ejb.Stateless
public class HotelMgmtEJB implements HotelMgmtRemote, HotelMgmtLocal {
@PersistenceContext(unitName="ejbjpa-hotel")
private EntityManager em;
private HotelDAO dao;
private HotelMgmt hotelMgmt;
@PostConstruct
public void init() {
dao = new JPAHotelDAO();
((JPAHotelDAO)dao).setEntityManager(em);
hotelMgmt = new HotelMgmtImpl();
((HotelMgmtImpl)hotelMgmt).setHotelDao(dao);
}
@Override
public Room getRoom(int number) {
return hotelMgmt.getRoom(number);
}
}
Performs stateless actions on server-side
Maintains no conversational state
Each method ignorant of what went before it and after it
Persistence Context re-instantiated with each call (Transaction-Scope)
Methods take in a set of parameters and return a result
May maintain implementation state when pooled
example: DataSource, JMS resources, references to other EJBs
Not the default behavior. Only an option when number of instances needed is limited and initialization is expensive
In no case should business state ever be maintained outside of the scope of a method or a back-end database or other stateful resource.
Examples:
TaxCalculator.calcTax(doubt amount)
BuyerMgmt.placeBid(int auctionId, double amount)
Dmv.getExpiredLicenses()
Good for implementing stateless access to resources
Contains no means to provide individualized access other than through separate deployments and call parameters
Figure 85.5. Example Stateful EJB
@javax.ejb.Stateful
public class ReservationEJB implements ReservationRemote {
@PersistenceContext(unitName="ejbjpa-hotel", type=PersistenceContextType.EXTENDED)
private EntityManager em;
@EJB
private HotelMgmtLocal hotelMgmt;
@Resource
private SessionContext ctx;
private List<Guest> guests = new LinkedList<Guest>();
@Override
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public int addGuest(Guest guest) {
if (guest!=null) {
guests.add(guest);
em.persist(guest); //<== no transaction active yet
}
return guests.size();
}
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRED)
@Remove
public List<Guest> reserveRooms() throws RoomUnavailableExcepton {
List<Room> rooms = hotelMgmt.getAvailableRooms(null, 0, guests.size());
if (rooms.size() < guests.size()) {
//no rollback needed, we didn't do anything
throw new RoomUnavailableExcepton(String.format("found on %d out of %d required",
rooms.size(), guests.size()));
}
//assign each one of them a room
List<Guest> completed = new ArrayList<Guest>(guests.size());
Iterator<Room> roomItr = rooms.iterator();
for (Guest guest: guests) {
Room room = roomItr.next();
try {
//the room could be unavailable -- depending on whether pessimistic lock created
guest = hotelMgmt.checkIn(guest, room); //<== will attempt to also persist guest
completed.add(guest);
} catch (RoomUnavailableExcepton ex) {
//rollback any previous reservations
ctx.setRollbackOnly();
throw ex;
}
}
return completed;
}
}
Used to cache resources for client on server-side
Maintains conversational state
Object can cache values between calls
example: iterator
Persistence Context (with cache of entities) can be retained between calls (Extended-Scope)
Lifetime ends on timeout or specific client call
Maintains implementation state
Not sharable between clients
All resources allocated to perform work for one instance
Able to react to transaction completion/rollback (i.e., get callback)
example: commit data cache
example: issue message
Good for caching client state and back-end resource state over a multi-call session
Inefficient to scale cached state for each user and across multiple servers
Figure 85.7. Example Singleton EJB
@javax.ejb.Singleton
@javax.ejb.Startup
@javax.ejb.ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
@javax.ejb.AccessTimeout(value=3000)
public class StatsEJB implements StatsLocal, StatsRemote {
private int total;
@Override
@javax.ejb.Lock(LockType.WRITE)
public void increment() { ... }
@Override
@javax.ejb.Lock(LockType.READ)
public int getTotal() { ... }
...
}
Provides a single Session EJB instance on server-side
Single instance shared across all clients
Has READ/WRITE lock and timeout concerns
Good for tracking shared state in memory
Creates a bottleneck for concurrent access
Figure 85.8. Example Message Driven EJB
@javax.ejb.MessageDriven(activationConfig={
@javax.ejb.ActivationConfigProperty(
propertyName="destinationType",
propertyValue="javax.jms.Topic"),
@javax.ejb.ActivationConfigProperty(
propertyName="destination",
propertyValue="java:/topic/ejava/examples/asyncMarket/topic1"),
@javax.ejb.ActivationConfigProperty(
propertyName="messageSelector",
propertyValue="JMSType in ('forSale', 'saleUpdate')"),
@javax.ejb.ActivationConfigProperty(
propertyName="acknowledgeMode",
propertyValue="Auto-acknowledge")
})
public class BuyerMDB implements MessageListener {
@javax.ejb.EJB
private BuyerLocal buyer;
@javax.annotation.security.PermitAll
public void onMessage(javax.jms.Message message) {
...
}
}
Used to receive JMS messages
Similar to Stateless EJB, but with no callable interface
Typically provides a JMS facade for injected Session EJBs
Good for implementing a JMS interface facade to business logic
Unable to be called outside the scope of a JMS/JCA call -- should delegate to Session EJBs
Has no client context (i.e., anonymous)
Can be assigned access roles using @javax.annotation.security.RunAs to be permitted to invoke methods of protected EJBs
Figure 85.9. Example Business Interface
public interface Teller {
Account createAccount(String accNum) throws BankException;
void updateAccount(Account account) throws BankException;
Account getAccount(String acctNum) throws BankException;
}
Pure POJO
No ties to EJB
No sense of local or remote -- just local
Figure 85.10. Example Remote Interface
@javax.ejb.Remote
public interface TellerRemote extends Teller {
Ledger getLedger() throws BankException;
List<Owner> getOwners(int index, int count) throws BankException;
String whoAmI();
}
public class Ledger implements java.io.Serializable {
...
}
Uses RMI-based technology to provide built-in implementation
Data types must be built-in types or Serializable types
Pass-by-value semantics between client and EJB
Usable by local (same application/EAR) and remote (outside of same application/EAR) endpoints
Pass-by-reference semantics between client and EJB
client and EJB share the same object instance
Must load class from same classloader
Data types need not be built-in or Serializable types
Restricted to be from application (e.g., must be from same EAR to share reference)
Remote clients are different from local clients and require different considerations when designing the remote interface. Although JavaEE/EJB make it technically possible to implement location independence the interface hierarchies, this is usually not feasible when dealing with non-trivial interface requirements. Design local interfaces for local clients (within the deployed application) and design remote interfaces for remote clients.
Figure 85.12. Example No Interface EJB
@javax.ejb.Singleton
@javax.ejb.Startup
@javax.ejb.ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
@javax.ejb.AccessTimeout(value=3000)
public class StatsEJB {
private int total;
@javax.ejb.Lock(LockType.WRITE)
public void increment() { ... }
@javax.ejb.Lock(LockType.READ)
public int getTotal() { ... }
...
}
Interfaces are an encapsulation issue and not an EJB issue
If you do not need the encapsulation of an interface -- EJB will *not* make you add one for local access
Must have interface when using remote clients
JAX-RS support
SOAP support
CORBA/IIOP support
Figure 85.13. Example JAX-RS Interface
import java.net.URI;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
@Path("/products")
public class ProductsResource {
@Inject
private InventoryMgmtEJB ejb;
@Context
private UriInfo uriInfo;
/**
* Updates a product with the values of the object passed in
* @param id
* @param product
* @return updated product if successful
*/
@PUT @Path("{id}")
@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@JsonbAnnotation //helps trigger JSON-B response marshaling when DTO also JAXB
public Response updateProduct(@PathParam("id") int id,
@JsonbProperty Product product //annotation helps trigger JSON-B
//request demarshaling when DTO also JAXB
) {
logger.debug("{} {}", request.getMethod(), uriInfo.getAbsolutePath());
try {
Product p = ejb.updateProduct(product);
logger.info("updated pojo product={}", p);
return Response.ok(p)
.tag("" + p.getVersion())
.build();
} catch (Exception ex) {
return ResourceHelper.serverError(logger, "update product", ex).build();
}
}
}
EJB modules are deployable by themselves
Contain no mechanism to bring in dependencies
Can only be communicated with using remote interface
Can deploy multiple EJB modules, multiple WARs, and utility libraries
Provides means to use local interfaces/pass by reference between EJBs and WAR/WEB
Convenient wrapper for entire application
Can deploy multiple EJB modules within WEB-INF/lib
Can deploy multiple EJB beans within WEB-INF/classes
Realistically useful when Servlet or JSP wishes to use declarative JTA transactions or security access control within a helper class
Ideal use for No-interface Session EJB
Eliminates need for EAR in many cases
Arranged in a parent/child relationship
May access classes/resources loaded by local and parent class loaders
Do not have access to classes/resources loaded by sibling class loaders
Requests for classes are first delegated to parent class loader
Classes loaded by one EJB deployment are not sharable with other deployments
Must use remote interfaces and pass-by-value serialized data
No pass by reference
EJB classes/resources can be shared with single Web App if loaded in WEB-INF/lib
Local interfaces/resources are local to that WAR
EJB classes cannot be shared across WARs
Must again resort to remote interfaces and pass-by-value serialized data to share implementations
EAR class loader is root to deployed application
EJB class loader is host to all EJBs and root to WAR class loaders
WAR class loaders are separate
Allows two separate WARs to generate the same class from two separate index.jsp files
Disallows WAR from duplicating EAR/EJB-supplied classes
EJBs and WARs-to-EJBs can share information using pass-by-reference
"JSR 345: Enterprise JavaBeansTM 3.2", Marina Vatkina, https://jcp.org/en/jsr/detail?id=345, 2013 Java Community Press.
"EJB Version History", Wikipedia, http://en.wikipedia.org/wiki/Enterprise_JavaBeans#Version_history, August 17, 2014.
Copyright © 2019 jim stafford (jim.stafford@jhu.edu)
Built on: 2019-08-22 07:11 EST
Abstract
This presentation provides information for developing a basic EJB (one without resource or dependency requirements) and deploying it to the server using WAR and EAR-based deployments.
Table of Contents
Show how EJBs build on POJO business logic concepts
Create and deploy a basic EJB (without resource requirements or other dependencies)
Show different EJB deployment options
Show how to access the EJB via a native EJB remote interface
Create an integration test for the EJB
At the completion of this topic, the student shall
be able to:
Create an EJB module
Create a simple @Stateless EJB in the EJB module
Add required and key aspects to the EJB
Unit test the EJB as a POJO
Deploy the EJB to the server using an EAR
Deploy the EJB to the server using a WAR
Integration test the EJB deployed to the server
EJBs are based on Plain Old Java Objects (POJOs)
Standard POJO rules apply...
...
public class GreeterEJB implements Greeter {
private static final Logger logger = LoggerFactory.getLogger(GreeterEJB.class);
...
}
Must not write to static fields. Read-only static fields are allowed (suggests use of final)
Must not use thread synchronization primitives to synchronize execution
Must not manage/manipulate threads, class loaders, or security managers
Must not attempt to become a network server and form a network listen and similar actions
...
@Override
public String sayHello(String name) throws BadRequestException {
logger.debug("sayHello({})", name);
if (name == null || name.isEmpty()) {
throw new BadRequestException("you must have a name for me to say hello");
}
return "hello " + name;
}
Normal stateless POJO method at this point
import info.ejava.examples.ejb.basic.dto.Greeting;
import info.ejava.examples.ejb.basic.dto.Name;
public interface Greeter {
String sayHello(String name) throws BadRequestException;
Greeting sayHello(Name name) throws BadRequestException;
}
Provides an abstraction of the public class business methods
Some methods use simple/primitive types. Others use complex/custom classes.
All types conveniently defined using Serializable types at this point to permit use of remote interface in future
public class Name implements Serializable {
private String firstName;
private String lastName;
...
public class Greeting implements Serializable {
private Date date;
private String message;
...
Data abstraction necessary for local and remote interaction
Serializable not necessary until we use this in a remote interface
public class BadRequestException extends Exception {
public BadRequestException(String message) {
super(message);
}
}
Checked exceptions used to report business rule failures
Unchecked exceptions used to report infrastructure issues
When designing exceptions or response codes for POJO business methods -- it would be good to include the concept of:
Not Found -- one or more of the targets of the action could not be found
Bad Request -- nothing will improve if same request issued again
Internal Error -- nothing wrong with request and may be succesful at a later date
Also attempt to include specifics about the failed request to include IDs and parameters in question. This information is very valuable in determining whether the client or server is at fault and to more quickly determine the specific error and how it can be resolved.
import static org.junit.Assert.*;
import info.ejava.examples.ejb.basic.dto.Greeting;
import info.ejava.examples.ejb.basic.dto.Name;
import info.ejava.examples.ejb.basic.ejb.BadRequestException;
import info.ejava.examples.ejb.basic.ejb.Greeter;
import info.ejava.examples.ejb.basic.ejb.GreeterEJB;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GreeterTest {
private static final Logger logger = LoggerFactory.getLogger(GreeterTest.class);
private Greeter greeter;
@Before
public void setUp() {
greeter = new GreeterEJB();
}
...
}
Test case used to verify the core functionality of the POJO -- prior to deployment as EJB
Each test case should be independent of another and not depend on a particular order
@Test
public void pojoGreeter() throws BadRequestException {
logger.info("*** pojoGreeter ***");
String name = "cat inhat";
String greeting = greeter.sayHello(name);
assertTrue("greeter did not say my name", greeting.contains(name));
}
@Test(expected = BadRequestException.class)
public void badName() throws BadRequestException {
logger.info("*** badName ***");
greeter.sayHello("");
}
@Test
public void dto() throws BadRequestException {
logger.info("*** dto ***");
Name name = new Name("thing", "one");
Greeting greeting = greeter.sayHello(name);
assertTrue("greeter did not say my name", greeting.getGreeting()
.contains(name.getFirstName()));
}
Each test method focused on testing a specific scenario or state
Contains assert statements to verify expected results
Each test method should be independent of another and not depend on a particular order
Production classes typically deployed in a Java archive (JAR)
$ jar tf target/ejb-basic-ejb-4.0.0-SNAPSHOT.jar ... info/ejava/examples/ejb/basic/ejb/BadRequestException.class info/ejava/examples/ejb/basic/ejb/Greeter.class info/ejava/examples/ejb/basic/ejb/GreeterEJB.class info/ejava/examples/ejb/basic/dto/Name.class info/ejava/examples/ejb/basic/dto/Greeting.class ...
|-- pom.xml `-- src |-- main | `-- java | `-- info | `-- ejava | `-- examples | `-- ejb | `-- basic | |-- dto | | |-- Greeting.java | | `-- Name.java | `-- ejb | |-- BadRequestException.java | |-- GreeterEJB.java | `-- Greeter.java `-- test |-- java | `-- info | `-- ejava | `-- examples | `-- ejb | `-- basic | `-- pojo | `-- GreeterTest.java `-- resources `-- log4j.xml
Module contains separate source trees for production and unit test code
Unit tests should run with every module build
Unit tests should auto-evaluate themselves so that no human analysis is required
$ mvn clean test ... [INFO] Deleting .../ejb-basic-ejb/target [INFO] [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ ejb-basic-ejb --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] skip non existing resourceDirectory .../ejb-basic-ejb/src/main/resources [INFO] [INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ ejb-basic-ejb --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 5 source files to .../ejb-basic-ejb/target/classes [INFO] [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ ejb-basic-ejb --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Copying 1 resource [INFO] [INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ ejb-basic-ejb --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 1 source file to .../ejb-basic-ejb/target/test-classes [INFO] [INFO] --- maven-surefire-plugin:2.17:test (default-test) @ ejb-basic-ejb --- [INFO] Surefire report directory: .../ejb-basic-ejb/target/surefire-reports ------------------------------------------------------- T E S T S ------------------------------------------------------- Running info.ejava.examples.ejb.basic.pojo.GreeterTest 16:57:12,237 INFO (GreeterTest.java:41) -*** dto *** 16:57:12,243 DEBUG (GreeterEJB.java:41) -sayHello(Name [firstName=thing, lastName=one]) 16:57:12,249 INFO (GreeterTest.java:27) -*** pojoGreeter *** 16:57:12,250 DEBUG (GreeterEJB.java:27) -sayHello(cat inhat) 16:57:12,252 INFO (GreeterTest.java:35) -*** badName *** 16:57:12,255 DEBUG (GreeterEJB.java:27) -sayHello() Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.239 sec - in info.ejava.examples.ejb.basic.pojo.GreeterTest Results : Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.815 s ...
Basic POJO/JAR pom thus far
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>info.ejava.examples.ejb.basicejb</groupId>
<artifactId>ejb-basic-ejb</artifactId>
<packaging>jar</packaging>
<dependencies>
<!-- core implementation dependencies -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Add essential EJB aspects to POJO classes
Deploy to server and test
import javax.ejb.Stateless;
...
@Stateless
public class GreeterEJB implements Greeter {
Other types include @Stateful, @Singleton, and @MessageDriven
import javax.ejb.Remote;
...
@Remote
public interface Greeter {
Defines business remote interface for EJB
Alternately can define @Remote(Interface.class) within EJB bean class
@Stateless
@Remote(Greeter.class)
public class GreeterEJB implements Greeter {
public interface Greeter {
String sayHello(String name) throws BadRequestException;
Greeting sayHello(Name name) throws BadRequestException;
}
import javax.ejb.Remote;
@Remote
public interface GreeterRemote extends Greeter {
}
Useful when EJB implements legacy interface
EJB must then implement Remote or declare @Remote
@Stateless
public class GreeterEJB implements GreeterRemote {
@Stateless
@Remote(GreeterRemote.class)
public class GreeterEJB implements Greeter {
Calling an interface a @Remote can mean a few things -- but it does not have to mean the direct caller is remote. It is recommended that you keep a separate definition of @Remote from @Local interfaces to be able to focus on the semantics of the data passed in and returned when implementing each interface. Remote interfaces should be designed to referenced entities by ID and to pass fully realized entities -- within the scope of the method intent -- by value. Local interfaces can pass lazily-loaded entities by reference. Remote method implementations could leverage sibling Local interface methods for most of their implementation. This allows Local interface EJBs to work fast-and-loose --avoiding excessive lookups -- while providing Remote interfaces at the transaction edge.
Optional callbacks to initialize and destroy resources
@PostConstruct
public void init() {
logger.info("*** GreeterEJB:init({}) ***", super.hashCode());
}
Called after all resources have been injected and before first business method called
No args, void return, no declared checked exceptions
Used to ready bean to satisfy business methods
@PreDestroy
public void destroy() {
logger.info("*** GreeterEJB:destroy({}) ***", super.hashCode());
}
If called, will be after last business method
EJB artifact looks very much like a normal POJO archive
Since our EJB is 100% annotation-based, we have no need for META-INF/ejb-jar.xml descriptor
$ jar tf target/ejb-basic-ejb-4.0.0-SNAPSHOT.jar ... info/ejava/examples/ejb/basic/ejb/BadRequestException.class info/ejava/examples/ejb/basic/ejb/Greeter.class info/ejava/examples/ejb/basic/ejb/GreeterEJB.class info/ejava/examples/ejb/basic/ejb/GreeterRemote.class info/ejava/examples/ejb/basic/dto/Name.class info/ejava/examples/ejb/basic/dto/Greeting.class ...
|-- pom.xml `-- src |-- main | `-- java | `-- info | `-- ejava | `-- examples | `-- ejb | `-- basic | |-- dto | | |-- Greeting.java | | `-- Name.java | `-- ejb | |-- BadRequestException.java | |-- GreeterEJB.java | |-- Greeter.java | `-- GreeterRemote.java `-- test |-- java | `-- info | `-- ejava | `-- examples | `-- ejb | `-- basic | `-- pojo | `-- GreeterTest.java `-- resources `-- log4j.xml
Module contains separate source trees for production and unit test code
EJB module deployed either as naked EJB
17:19:52,979 INFO [org.jboss.as.ejb3.deployment.processors.EjbJndiBindingsDeploymentUnitProcessor] (MSC service thread 1-1) JNDI bindings for session bean named GreeterEJB in deployment unit deployment "ejb-basic-ejb.jar" are as follows: java:global/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:app/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:module/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:jboss/exported/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:global/ejb-basic-ejb/GreeterEJB java:app/ejb-basic-ejb/GreeterEJB java:module/GreeterEJB 17:19:53,012 INFO [org.jboss.weld.deployer] (MSC service thread 1-1) JBAS016005: Starting Services for CDI deployment: ejb-basic-ejb.jar 17:19:53,029 INFO [org.jboss.weld.deployer] (MSC service thread 1-2) JBAS016008: Starting weld service for deployment ejb-basic-ejb.jar 17:19:53,485 INFO [org.jboss.as.server] (DeploymentScanner-threads - 1) JBAS018559: Deployed "ejb-basic-ejb.jar" (runtime-name : "ejb-basic-ejb.jar")
JBoss server prints out JNDI names to access EJB through...
@Remote
No interface
Globally within the server (java:global)
Within the deployed application (java:app)
Within the EJB module (java:module)
External to server (java:global/exported)
Derive remote client JNDI name from "exported" name
ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
Local and No-interface interfaces are not exposed to remote clients
packaging=ejb
Required in order for downstream containers to recognize as EJB component
javax.ejb:javax.ejb-api
Supplies EJB API definitions
maven-ejb-plugin
Builds EJB and optional ejbclient artifact
<project>
...
<packaging>ejb</packaging>
<dependencies>
...
<dependency>
<groupId>javax.ejb</groupId>
<artifactId>javax.ejb-api</artifactId>
<scope>provided</scope>
</dependency>
...
</dependencies>
<build>
<plugins>
<!-- tell EJB plugin to create a client-jar -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ejb-plugin</artifactId>
<configuration>
<generateClient>true</generateClient>
<clientExcludes>
<clientExclude>**/ejb/*Local.class</clientExclude>
<clientExclude>**/ejb/*EJB.class</clientExclude>
</clientExcludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Initialized EJB bean instances can be cached in a pool
Instances from the pool can be re-used rather than destroy and re-intialize a new instance
JBoss pools singleton and stateful session beans by default
# standalone/configuration/standalone.xml
<session-bean>
<stateful default-access-timeout="5000" cache-ref="simple" passivation-disabled-cache-ref="simple"/>
<singleton default-access-timeout="5000"/>
</session-bean>
JBoss defines a stateless session bean pool for use but left unassigned
# standalone/configuration/standalone.xml
<pools>
<bean-instance-pools>
<strict-max-pool name="slsb-strict-max-pool" max-pool-size="20" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
<strict-max-pool name="mdb-strict-max-pool" max-pool-size="20" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
</bean-instance-pools>
</pools>
No pooling can be costly when initialization costs are high
Stateless and MDB EJB pooling can be enabled globally
# standalone/configuration/standalone.xml
<session-bean>
<stateless>
<bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>
</stateless>
<stateful default-access-timeout="5000" cache-ref="simple" passivation-disabled-cache-ref="simple"/>
<singleton default-access-timeout="5000"/>
</session-bean>
Pooling can be costly when initialization costs are low
Stateless and MDB EJB pooling can be enabled on a per-application(*)/per-EJB(GreeterEJB) basis
# META-INF/jboss-ejb3.xml
<jboss:ejb-jar xmlns:jboss="http://www.jboss.com/xml/ns/javaee"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:s="urn:security"
xmlns:c="urn:clustering:1.0"
xmlns:p="urn:ejb-pool:1.0"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-ejb3-2_0.xsd
http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd"
version="3.1"
impl-version="2.0">
<assembly-descriptor>
<!-- defines EJB pool else not pooled -->
<p:pool>
<ejb-name>*</ejb-name>
<p:bean-instance-pool-ref>slsb-strict-max-pool</p:bean-instance-pool-ref>
</p:pool>
</assembly-descriptor>
</jboss:ejb-jar>
Added necessary aspects to POJO business logic to make it an EJB
Annotated class as @Stateless
Annotated remote interface as @Remote
Deployed to server
Obtained JNDI name of naked EJB-based @Remote
(component-name)/(ejb-name)!(remote-interface-name)
ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
Useful when need to deploy EJB with dependent artifacts or without WAR
|-- ejb-basic-ejb.jar `-- META-INF `-- application.xml
Must supply a META-INF/application.xml with the EJB listed as an ejb module
May supply utility jars in library-directory (e.g., /lib)
$ more META-INF/application.xml
<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/application_7.xsd" version="7">
<application-name>ejb-basic-ear</application-name>
<description>This projects provides an example EAR for deploying
one or more EJB and other type modules.
This EAR is assembled here, but tested as a part of the
function/integration testing in a sibling project.</description>
<display-name>ejb-basic-ear</display-name>
<module>
<ejb>ejb-basic-ejb.jar</ejb>
</module>
<library-directory>lib</library-directory>
</application>
EJB module deployed within EAR
19:50:07,583 INFO [org.jboss.as.ejb3.deployment.processors.EjbJndiBindingsDeploymentUnitProcessor] (MSC service thread 1-1) JNDI bindings for session bean named GreeterEJB in deployment unit subdeployment "ejb-basic-ejb.jar" of deployment "ejb-basic-ear-4.0.0-SNAPSHOT.ear" are as follows: java:global/ejb-basic-ear/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:app/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:module/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:jboss/exported/ejb-basic-ear/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:global/ejb-basic-ear/ejb-basic-ejb/GreeterEJB java:app/ejb-basic-ejb/GreeterEJB java:module/GreeterEJB 19:50:07,588 INFO [org.jboss.weld.deployer] (MSC service thread 1-1) JBAS016005: Starting Services for CDI deployment: ejb-basic-ear-4.0.0-SNAPSHOT.ear 19:50:07,596 INFO [org.jboss.weld.deployer] (MSC service thread 1-2) JBAS016008: Starting weld service for deployment ejb-basic-ear-4.0.0-SNAPSHOT.ear
JBoss server prints out JNDI names to access EJB through...
@Remote
No interface
Globally within the server (java:global)
Within the deployed application (java:app)
Within the EJB module (java:module)
External to server (java:global/exported)
Derive remote client JNDI name from "exported" name
ejb-basic-ear/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
Local and No-interface interfaces are not exposed to remote clients
packaging=ear
Configures plugin to perform EAR-like packaging of dependencies
ejb scope=compile dependencies
Name upstream Maven modules that should be deployed as EJBs
maven-ear-plugin
Can perform most actions be default/convention. Example shows how to clean up JNDI names so that version# is not part of the JNDI name exposed
<project>
<groupId>info.ejava.examples.ejb.basicejb</groupId>
<artifactId>ejb-basic-ear</artifactId>
<packaging>ear</packaging>
<dependencies>
<!-- The EAR must have a scope=compile dependency on the EJB -->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>ejb-basic-ejb</artifactId>
<version>${project.version}</version>
<type>ejb</type>
</dependency>
</dependencies>
<build>
<plugins>
<!-- provide properties here to impact the EAR packaging -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ear-plugin</artifactId>
<configuration>
<defaultLibBundleDir>lib</defaultLibBundleDir>
<!-- eliminates use of version in EAR JNDI name portion -->
<applicationName>${project.artifactId}</applicationName>
<modules>
<!-- eliminates use of the version in the EJB JNDI name -->
<ejbModule>
<groupId>${project.groupId}</groupId>
<artifactId>ejb-basic-ejb</artifactId>
<bundleFileName>ejb-basic-ejb.jar</bundleFileName>
</ejbModule>
</modules>
</configuration>
</plugin>
</plugins>
</build>
Add optional undeploy of EAR to clean lifecycle
Must be done in pre-clean phase before .ear artifact removed
Latch in profile (-Pundeploy) to prevent from running when .ear not exists
<profiles>
<profile>
<id>undeploy</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<executions>
<execution>
<id>undeploy-ear</id>
<phase>pre-clean</phase>
<goals>
<goal>undeploy</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
Add EAR artifact as a scope=compile dependency when deploying from separate (test) module
<dependencies>
...
<!-- packages being deployed must be a dependency -->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>ejb-basic-ear</artifactId>
<version>${project.version}</version>
<type>ear</type>
<scope>compile</scope>
</dependency>
...
</dependencies>
Declare EAR artifact to be deployed
<build>
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<configuration>
<!-- artifacts to deploy to server -->
<deployables>
<deployable>
<groupId>${project.groupId}</groupId>
<artifactId>ejb-basic-ear</artifactId>
<type>ear</type>
</deployable>
</deployables>
</configuration>
</plugin>
</plugins>
</build>
Details of cargo-maven2-plugin are provided elsewhere
Deployed EJB using an EAR
Traditional JavaEE deployment
Permits deployment of dependency artifacts
Obtained JNDI name of EAR-based @Remote
(application-name)/(component-name)/(ejb-name)!(remote-interface-name)
ejb-basic-ear/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
Useful in WAR deployments
`-- WEB-INF `-- WEB-INF |-- classes | `-- info | `-- ejava | `-- examples | `-- ejb | `-- basic | `-- webejb | `-- WebGreeterEJB.class |-- jboss-web.xml `-- lib `-- ejb-basic-ejb-4.0.0-SNAPSHOT.jar
WARs may host
imported EJBs - developed in a separate EJB module
embedded EJBs - developed within this module
(WebGreeterEJB is a copy/paste clone of GreeterEJB)
EJB module deployed within WAR
20:47:51,929 INFO [org.jboss.as.ejb3.deployment.processors.EjbJndiBindingsDeploymentUnitProcessor] (MSC service thread 1-2) JNDI bindings for session bean named GreeterEJB in deployment unit deployment "ejb-basic-war.war" are as follows: java:global/ejb-basic-war/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:app/ejb-basic-war/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:module/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:jboss/exported/ejb-basic-war/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:global/ejb-basic-war/GreeterEJB java:app/ejb-basic-war/GreeterEJB java:module/GreeterEJB 20:47:51,970 INFO [org.jboss.weld.deployer] (MSC service thread 1-2) JBAS016005: Starting Services for CDI deployment: ejb-basic-war.war 20:47:51,988 INFO [org.jboss.weld.deployer] (MSC service thread 1-1) JBAS016008: Starting weld service for deployment ejb-basic-war.war 20:47:52,353 INFO [org.wildfly.extension.undertow] (MSC service thread 1-1) JBAS017534: Registered web context: /ejb-basic-war
JBoss server prints out JNDI names to access EJB through...
@Remote
No interface
Globally within the server (java:global)
Within the deployed application (java:app)
Within the EJB module (java:module)
External to server (java:global/exported)
Derive remote client JNDI name from "exported" name
ejb-basic-war/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
Local and No-interface interfaces are not exposed to remote clients
Embedded EJBs have same naming structure
java:global/ejb-basic-war/WebGreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:app/ejb-basic-war/WebGreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:module/WebGreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:jboss/exported/ejb-basic-war/WebGreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:global/ejb-basic-war/WebGreeterEJB java:app/ejb-basic-war/WebGreeterEJB java:module/WebGreeterEJB
packaging=war
Configures plugin to perform WAR-like packaging of dependencies
scope=compile dependencies
Name upstream Maven modules that should be deployed in WEB-INF/classes
build.finalName
Helps eliminate version# from being in JNDI name
<project>
<groupId>info.ejava.examples.ejb.basicejb</groupId>
<artifactId>ejb-basic-war</artifactId>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>ejb-basic-ejb</artifactId>
<version>${project.version}</version>
<type>ejb</type>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<finalName>ejb-basic-war</finalName>
</build>
</project>
If the WAR is ever deployed within an EAR with a sibling EJB as its dependency -- the scope of the dependency should be defined as "provided" and not "compile". The "compile" scope will always deploy named artifacts (and their dependencies) within the WEB-INF/lib directory of the WAR. If the EAR is deploying them as well -- you will end up with duplicate deployments (one from the EAR and one from the WAR) and will eventally fail.
Support for embedded EJB development will require standard POJO and EJB dependencies
<!-- for embedded code/EJBs -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.ejb</groupId>
<artifactId>javax.ejb-api</artifactId>
<scope>provided</scope>
</dependency>
Add EAR artifact as a scope=compile dependency when deploying from separate (test) module
<dependencies>
...
<!-- packages being deployed must be a dependency -->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>ejb-basic-war</artifactId>
<version>${project.version}</version>
<type>war</type>
<scope>compile</scope>
</dependency>
...
</dependencies>
Declare EAR artifact to be deployed
<build>
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<configuration>
<!-- artifacts to deploy to server -->
<deployables>
<deployable>
<groupId>${project.groupId}</groupId>
<artifactId>ejb-basic-war</artifactId>
<type>war</type>
</deployable>
</deployables>
</configuration>
</plugin>
</plugins>
</build>
Details of cargo-maven2-plugin are provided elsewhere
Unit tests test POJO classes in a single Maven phase
IT tests test components deployed to the server using multiple phases Maven Lifecycle Reference
pre-integration-test
Deploy artifacts
integration-test
Execute tests
post-integration-test
Undeploy artifacts
verify
Evaluate test results (possibly fail build after artifacts undeployed)
IT tests require remote access
Access Type
JBoss Remoting
EJB Client
JNDI Properties
JNDI Name
IT tests test EJB deployed to server
Java client may access @Remote interface of EJB by first going through Java Naming and Directory Interface (JNDI)
JBoss uses two different types of remote interface techniques -- both accessed thru JNDI; JBoss Remoting and EJB Client
Obtain JNDI properties through combination of jndi.properties file and supplied Properties object.
java.util.Properties jndiProperties=...; // varies whether using Remoting or EJBClient
|-- jndi.properties java.naming.factory.initial=... java.naming.factory.url.pkgs=... java.naming.provider.url=... #java.naming.security.principal=... #java.naming.security.credentials=...
Security properties shown here for completeness. Can be statically defined.
Get InitialContext to server JNDI tree
Context jndi = new InitialContext(jndiProperties); //jndi.properties augmented
Context jndi = new InitialContext(); //jndi.properties is complete
Obtain JNDI-name using deployment-specific name and access technique
String jndiName; // varies whether accessing EJB, WAR or EAR deployment and access
Lookup EJB's remote interface
GreeterRemote greeter = (GreeterRemote) jndi.lookup(jndiName);
Legacy approach
Follows published standards
Consistent with other resource types (e.g., JMS resource lookups)
Knows nothing of EJB
Stated to be less efficient than EJB Client
java.naming.factory.initial=org.wildfly.naming.client.WildFlyInitialContextFactory java.naming.factory.url.pkgs= java.naming.provider.url=http-remoting://127.0.0.1:8080 #java.naming.security.principal=known #java.naming.security.credentials=password1!
Credentials are strictly for JNDI lookups
Credentials are optional when client connecting from same machine as server (i.e., development mode)
ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
ejb-basic-war/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
ejb-basic-ear/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
ejb-basic-war/WebGreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
Custom API for accessing EJBs
Can perform certain actions more efficiently with that knowledge and specialization
Uses separate naming context (ejb:)
Uses different and separate properties
Specifics can be shielded from client with minimal effort
#java.naming.factory.initial java.naming.factory.url.pkgs=org.jboss.ejb.client.naming java.naming.provider.url=http-remoting://127.0.0.1:8080
java.naming.factory.initial
can be anything legal and not blank
The WildFlyInitialContextFactory can handle both Remoting and EJB Client JNDI setup with the same jndi.properties configuration. The specific extension shown above was necessary in past versions and only shown here as an example to show more details of how the "ejb:" prefix and "java.naming.factory.url.pkgs" is being used.
EJB Client contains other details like alternate URLs, authentication configuration, and encryption options that go beyond basic communications but address more detailed use of remote interfaces. These details are configured in a separate configuration file that is specific to JBoss and will not be covered here.
ejb:/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
ejb:/ejb-basic-war/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
ejb:ejb-basic-ear/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
ejb:/ejb-basic-war/WebGreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
The EJB Client implementation understands JBoss/Wildfly EJBs and to further help optimize the communication with Stateful Session EJBs the following string ("?stateful")must be appended to the JNDI name
InitialContextFactory looks for custom naming extensions when encountering naming prefix ("ejb:")
java.naming.factory.url.pkgs lists base package names to start looking for extensions
jboss-ejb-client.jar must be in the classpath
Provider extracts "ejb" prefix from "ejb:/..." JNDI name
ejb:/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
Provider obtains list of Java package prefixes from "jndi.naming.factory.url.pkgs"
#jndi.properties java.naming.factory.url.pkgs=org.jboss.ejb.client.naming
Provider searches for "org.jboss.ejb.client.naming.ejb" Java package in classpath
$ mvn dependency:tree ... [INFO] +- info.ejava.examples.common:jboss-rmi-client:pom:5.0.0-SNAPSHOT:test [INFO] | | +- org.jboss:jboss-ejb-client:jar:4.0.10.Final:test [INFO] | | +- org.jboss.remoting:jboss-remoting:jar:5.0.5.Final:test [INFO] | | +- org.wildfly:wildfly-naming-client:jar:1.0.9.Final:test [INFO] | | +- org.wildfly.wildfly-http-client:wildfly-http-naming-client:jar:1.0.12.Final:test ...
Provider locates an ObjectFactory class in the "org.jboss.ejb.client.naming" Java package that starts with "ejb"
$ jar tf ~/.m2/repository/org/jboss/jboss-ejb-client/4.0.10.Final/jboss-ejb-client-4.0.10.Final.jar | grep org.jboss.ejb.client.naming org/jboss/ejb/client/naming/ejb/ejbURLContextFactory.class
Provider uses that ObjectFactory to resolve the remaining portion of the JNDI name
ejb:/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote
EJB Client only works for EJBs
InitialContext may need to locate other non-EJB resources (e.g., JMS)
Replace default InitialContextFactory with JBoss Remoting
Extend JBoss Remoting InitialContextFactory with EJB Client
java.naming.factory.initial=org.jboss.naming.remote.client.InitialContextFactory java.naming.factory.url.pkgs=org.jboss.ejb.client.naming java.naming.provider.url=http-remoting://localhost:8080
However, ejbURLContextFactory is deprecated in favor of using WildFlyInitialContextFactory. The following is a bit easier to remember and configure.
java.naming.factory.initial=org.wildfly.naming.client.WildFlyInitialContextFactory java.naming.factory.url.pkgs= java.naming.provider.url=http-remoting://localhost:8080
IT tests require...
Server running (possibly started)
EJB deployed
Tests run
EJB undeployed (proper cleanup)
Server stopped (if started as part of the test)
Results evaluated
IT test can also be written with JUnit
@BeforeClass runs at start of test case and before all test methods. This must be public and static
@Before runs before each test method. This must be public and non-static
@After runs after each test method. This must be public and non-static
@AfterClass runs after all test methods are complete and prior to test case shutdown. This must be public and static
...
import static org.junit.Assert.*;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import info.ejava.examples.ejb.basic.dto.Greeting;
import info.ejava.examples.ejb.basic.dto.Name;
import info.ejava.examples.ejb.basic.ejb.BadRequestException;
import info.ejava.examples.ejb.basic.ejb.GreeterRemote;
...
public class GreeterIT {
private static final Logger logger = LoggerFactory.getLogger(GreeterBase.class);
protected Properties jndiProperties; // varies whether using Remoting or EJBClient
protected String jndiName; // varies whether accessing EJB, WAR or EAR deployment and access
protected Context jndi;
protected GreeterRemote greeter;
@Before
public void setUp() throws Exception {
jndi = new InitialContext(jndiProperties);
greeter = (GreeterRemote) jndi.lookup(jndiName);
}
@After
public void tearDown() throws Exception {
if (jndi != null) {
jndi.close(); // produces errors with JBoss Remoting
}
}
@Before method sets up JNDI context and performs lookup for @Remote before each @Test
@After method closes out JNDI Context between @Tests
Same as before except they must use the EJB remote interface rather than instantiate bean
@Test
public void pojoGreeter() throws BadRequestException {
logger.info("*** pojoGreeter ***");
String name = "cat inhat";
String greeting = greeter.sayHello(name);
assertTrue("greeter did not say my name", greeting.contains(name));
}
@Test(expected = BadRequestException.class)
public void badName() throws BadRequestException {
logger.info("*** badName ***");
greeter.sayHello("");
}
@Test
public void dto() throws BadRequestException {
logger.info("*** dto ***");
Name name = new Name("thing", "one");
Greeting greeting = greeter.sayHello(name);
assertTrue("greeter did not say my name", greeting.getGreeting()
.contains(name.getFirstName()));
}
IT tests housed in separate modules (can be part of EJB and WAR modules)
jndi.properties part of IT test tree -- not the production code
. |-- pom.xml `-- src `-- test |-- java | `-- info | `-- ejava | `-- examples | `-- ejb | `-- basicejb | `-- ejbclient | |-- GreeterIT.java `-- resources |-- jndi.properties `-- log4j.xml
surefire/Unit Tests run prior to deployment
failsafe/IT tests run after deployment
$ mvn clean verify ... [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ ejb-basic-test --- [INFO] Deleting .../ejb-basic-example/ejb-basic-test/target ... [INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ ejb-basic-test --- [INFO] No sources to compile [INFO] [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ ejb-basic-test --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Copying 4 resources [INFO] [INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ ejb-basic-test --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 5 source files to .../ejb-basic-example/ejb-basic-test/target/test-classes [INFO] [INFO] --- maven-surefire-plugin:2.17:test (default-test) @ ejb-basic-test --- [INFO] [INFO] --- maven-jar-plugin:2.5:jar (default-jar) @ ejb-basic-test --- [WARNING] JAR will be empty - no content was marked for inclusion! [INFO] Building jar: .../ejb-basic-example/ejb-basic-test/target/ejb-basic-test-4.0.0-SNAPSHOT.jar ... [INFO] --- cargo-maven2-plugin:1.4.3:redeploy (cargo-prep) @ ejb-basic-test --- Sep 28, 2014 11:58:23 PM org.xnio.Xnio <clinit> INFO: XNIO version 3.2.2.Final Sep 28, 2014 11:58:23 PM org.xnio.nio.NioXnio <clinit> INFO: XNIO NIO Implementation Version 3.2.2.Final Sep 28, 2014 11:58:23 PM org.jboss.remoting3.EndpointImpl <clinit> INFO: JBoss Remoting version 4.0.3.Final ... [INFO] --- maven-failsafe-plugin:2.17:integration-test (integration-tests) @ ejb-basic-test --- [INFO] Failsafe report directory: .../ejb-basic-example/ejb-basic-test/target/failsafe-reports ------------------------------------------------------- T E S T S ------------------------------------------------------- Running info.ejava.examples.ejb.basicejb.ejbclient.GreeterRemotingWARIT 23:58:26,441 INFO (GreeterIT.java:43) -using jndiName=ejb-basic-war/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote ... 23:58:27,811 INFO (GreeterIT.java:43) -using jndiName=ejb:ejb-basic-ear/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote ... 23:58:28,167 INFO (GreeterIT.java:43) -using jndiName=ejb-basic-ear/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote ... 23:58:28,289 INFO (GreeterIT.java:43) -using jndiName=ejb:/ejb-basic-war/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote Results : Tests run: 12, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] --- cargo-maven2-plugin:1.4.3:undeploy (cargo-post) @ ejb-basic-test --- [INFO] [INFO] --- maven-failsafe-plugin:2.17:verify (verify) @ ejb-basic-test --- [INFO] Failsafe report directory: .../ejb-basic-example/ejb-basic-test/target/failsafe-reports [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 11.036 s
Copyright © 2019 jim stafford (jim.stafford@jhu.edu)
Built on: 2019-08-22 07:11 EST
Abstract
This presentation provides information for performing use cases that are specific to developing server-side EJBs. Topics range from managing server execution, executing the build, and remote debugging.
Manage server execution
Manage EJB deployment
Running IT tests with server deployments
Perform remote debugging
At the completion of this topic, the student shall
be able to:
Choose whether to run application server standalone or embedded
Start/Stop application server
Select specific Maven commands to execute for the development case
Run an IT test within IDE with EJB deployed to application server
Remote debug EJB deployed to application server
Two primary ways to run the application server
Standalone. Can be local or remote
Embedded within IDE. Shares same JVM.
Deployed applications log to an API (e.g., commons-logging, slf4j)
API bound to server logger mechanism (e.g., log4j)
Figure 91.1. Adjust Verbosity for Application
# standalone/configuration/standalone.xml
<logger category="info.ejava">
<level name="DEBUG"/>
</logger>
<root-logger>
<level name="INFO"/>
<handlers>
<handler name="CONSOLE"/>
<handler name="FILE"/>
</handlers>
</root-logger>
root-logger sets default versbosity to INFO
additional logger sets verbosity for info.ejava.* to DEBUG
Figure 91.2. Application Code
package info.ejava.examples.ejb.basic.ejb;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Stateless
@Remote(GreeterRemote.class)
public class GreeterEJB implements Greeter {
private static final Logger logger = LoggerFactory.getLogger(GreeterEJB.class);
@PostConstruct
public void init() {
logger.info("*** GreeterEJB ***");
}
@Override
public String sayHello(String name) throws BadRequestException {
logger.debug("sayHello({})", name);
...
}
EJB class used to create logger named after the class' fully qualified name
@PostConstruct logging at INFO level
business method logging at DEBUG level
Figure 91.3. Server Console Output
./bin/standalone.sh ... 01:17:01,990 INFO [info.ejava.examples.ejb.basic.ejb.GreeterEJB] (EJB default - 1) *** GreeterEJB ***
Console configured (by default) to only output INFO and above
Figure 91.4. Server log/server.log Output
$ tail -n 999 -f standalone/log/server.log 2014-10-01 01:17:01,990 INFO [info.ejava.examples.ejb.basic.ejb.GreeterEJB] (EJB default - 1) *** GreeterEJB *** 2014-10-01 01:17:02,009 DEBUG [info.ejava.examples.ejb.basic.ejb.GreeterEJB] (EJB default - 1) sayHello(cat inhat)
server.log will print all verbosity levels
Runs as separate process
Started/stopped by shell commands
Decoupled from IDE
Use start script (standalone.sh or standalone.bat)
Figure 91.5. Start Standalone Server at Shell
wildfly-13.0.0.Final$ ./bin/standalone.sh -c standalone.xml ========================================================================= JBoss Bootstrap Environment JBOSS_HOME: /Users/jim/apps/wildfly-13.0.0.Final JAVA: java JAVA_OPTS: -server -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true ========================================================================= 10:14:01,139 INFO [org.jboss.modules] (main) JBoss Modules version 1.8.5.Final 10:14:01,372 INFO [org.jboss.msc] (main) JBoss MSC version 1.4.2.Final 10:14:01,379 INFO [org.jboss.threads] (main) JBoss Threads version 2.3.2.Final 10:14:01,474 INFO [org.jboss.as] (MSC service thread 1-2) WFLYSRV0049: WildFly Full 13.0.0.Final (WildFly Core 5.0.0.Final) starting ... 10:14:03,969 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: WildFly Full 13.0.0.Final (WildFly Core 5.0.0.Final) started in 3104ms - Started 358 of 581 services (356 services are lazy, passive or on-demand)
Server can be started at the command line
Add -c profile-name.xml to run alternate profile in standalone/configuration
Server can be safely shutdown using the command line shell or Control-C
Figure 91.6. Shutdown Standalone Server using Command
wildfly-13.0.0.Final$ ./bin/jboss-cli.sh --connect command=:shutdown { "outcome" => "success", "result" => undefined }
All server actions can be scripted with command-line interface
Figure 91.7. Kill Standalone Server using Control-C
^C10:17:19,557 INFO [org.jboss.as.server] (Thread-2) WFLYSRV0236: Suspending server with no timeout. 10:17:19,558 INFO [org.jboss.as.ejb3] (Thread-2) WFLYEJB0493: EJB subsystem suspension complete 10:17:19,559 INFO [org.jboss.as.server] (Thread-2) WFLYSRV0220: Server shutdown has been requested via an OS signal ... 10:17:19,616 INFO [org.jboss.as] (MSC service thread 1-1) WFLYSRV0050: WildFly Full 13.0.0.Final (WildFly Core 5.0.0.Final) stopped in 53ms
Server can also be safely shutdown with Control-C in console window
Runs within IDE JVM
Controlled through UI clicks
Output appears in IDE panels
Provides easier access to server-side debugging and profiling
Install JBoss Tools (JBoss AS Tools) -- shown earlier
Figure 91.8. Choose New Server
Locate Server tab in the Eclipse JavaEE Profile
Right-Click in panel and select New Server
Choose most recent Wildfly configuration closest to current
Start (debug and profile) option associated with server
Console output appears in IDE console window
Many/most build commands can be automated through Maven or other scripts
Used in interactive development and automated/headless builds
Uses EJB, EAR and Test Module
Also shows WAR option to EAR
IT tests could be placed inside WAR module
Figure 92.1. Example Source Tree
. |-- ejb-basic-ear | `-- pom.xml |-- ejb-basic-ejb | |-- pom.xml | `-- src | |-- main | | `-- java | | `-- info | | `-- ejava | | `-- examples | | `-- ejb | | `-- basic | | |-- dto | | | |-- Greeting.java | | | `-- Name.java | | `-- ejb | | |-- BadRequestException.java | | |-- GreeterEJB.java | | |-- Greeter.java | | `-- GreeterRemote.java | `-- test | |-- java | | `-- info | | `-- ejava | | `-- examples | | `-- ejb | | `-- basic | | `-- pojo | | `-- GreeterTest.java | `-- resources | `-- log4j.xml |-- ejb-basic-test | |-- pom.xml | `-- src | `-- test | |-- java | | `-- info | | `-- ejava | | `-- examples | | `-- ejb | | `-- basicejb | | `-- ejbclient | | `-- GreeterIT.java | `-- resources | |-- jndi.properties | `-- log4j.xml |-- ejb-basic-war | |-- pom.xml | `-- src | `-- main | `-- webapp | `-- WEB-INF | `-- jboss-web.xml `-- pom.xml
Tests functionality of code within the bounds of a single JVM
All external dependencies stubbed, simulated, or in-memory versions used (e.g., database, JMS server)
Copies resource files from src tree to target tree
Can optionally "filter" variables and replace them with build time values (from environment's settings.xml)
Useful in customizing environment-specific properties
server URLs
server port#s
Figure 92.2. Example Source Resource File: src/test/resources/jndi.properties
$ cat src/test/resources/jndi.properties
java.naming.factory.initial=${java.naming.factory.initial}
java.naming.factory.url.pkgs=${java.naming.factory.url.pkgs}
java.naming.provider.url=${java.naming.provider.url}
#java.naming.security.principal=${jndi.user}
#java.naming.security.credentials=${jndi.password}
Source file uses variables to template source file
Figure 92.3. Example Filtered Resource File: target/test-classes/jndi.properties
$ cat target/test-classes/jndi.properties
java.naming.factory.initial=org.wildfly.naming.client.WildFlyInitialContextFactory
java.naming.factory.url.pkgs=
java.naming.provider.url=http-remoting://127.0.0.1:8080
#java.naming.security.principal=known
#java.naming.security.credentials=password1!
Concrete values are put in place for specific environment
Figure 92.4. Example Maven Configuration
<properties>
<jboss.host>localhost</jboss.host>
<jboss.http.port>8080</jboss.http.port>
<java.naming.factory.initial>org.wildfly.naming.client.WildFlyInitialContextFactory</java.naming.factory.initial>
<java.naming.provider.url>http-remoting://${jboss.host}:${jboss.http.port}</java.naming.provider.url>
<java.naming.factory.url.pkgs/>
<jndi.user>known</jndi.user>
<jndi.password>password1!</jndi.password>
</properties>
<build>
<!-- filter test/resource files for profile-specific valies -->
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
</testResource>
</testResources>
Properties defined either within pom.xml, settings.xml, or -Dsystem-properties
includes/exclused aids in filtering wrong files
avoid filtering binary files -- corrupts them (Ant has same issue)
Runs unit tests
Runs after tests compiled and before IT tests
Figure 92.5. Module Declaration
<properties>
<loop-count>3</loop-count>
</properties>
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<loop.count>${loop-count}</loop.count>
</systemPropertyVariables>
</configuration>
</plugin>
Plugin always enabled by default. Declaration used only to customize
Configuration defines environment for JVM
Easy way to supply system properties (-Dsystem-property=value)
values can be hard-coded or use properties to allow easier overrides
Figure 92.6. Optional Includes/Excludes
<configuration>
<includes>
<include>**/GreeterTest.java</include>
</includes>
</configuration>
Useful when turning off all unit tests to concentrate on IT tests
Can turn off problem test(s)
Figure 92.7. Parent Definition
<maven-surefire-plugin.version>2.17</maven-surefire-plugin.version>
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<argLine>${surefire.argLine}</argLine>
</configuration>
</plugin>
Just defining version here
Figure 92.8. Run Unit Tests
$ mvn clean test ... [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ ejb-basic-ejb --- ... [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ ejb-basic-ejb --- ... [INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ ejb-basic-ejb --- ... [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ ejb-basic-ejb --- ... [INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ ejb-basic-ejb --- ... [INFO] --- maven-surefire-plugin:2.17:test (default-test) @ ejb-basic-ejb --- ------------------------------------------------------- T E S T S ------------------------------------------------------- Running info.ejava.examples.ejb.basic.pojo.GreeterTest 02:11:38,695 INFO (GreeterTest.java:41) -*** dto *** 02:11:38,701 DEBUG (GreeterEJB.java:45) -sayHello(Name [firstName=thing, lastName=one]) 02:11:38,707 INFO (GreeterTest.java:27) -*** pojoGreeter *** 02:11:38,708 DEBUG (GreeterEJB.java:31) -sayHello(cat inhat) 02:11:38,711 INFO (GreeterTest.java:35) -*** badName *** 02:11:38,713 DEBUG (GreeterEJB.java:31) -sayHello() Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.238 sec - in info.ejava.examples.ejb.basic.pojo.GreeterTest Results : Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.871 s
Just defining version here
Runs after artifacts packaged and deployed
Required when tests require preparation/shutdown external to JVM
Four (4) phases
pre-integration-test
integration-test
post-integration-test
verify
Can be in same module as deployable artifact (i.e., within EJB or WAR modules)
Manages deployment of components to containers
Not specific to JBoss/Wildfly
Specializations for Maven and JBoss/Wildfly
Can run embedded within each IT module build or use remote instance
Typically configured within module with IT tests
Figure 92.9. IT Module Declaration
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<configuration>
<!-- artifacts to deploy to server -->
<deployables>
<deployable>
<groupId>${project.groupId}</groupId>
<artifactId>ejb-basic-ear</artifactId>
<type>ear</type>
</deployable>
</deployables>
</configuration>
</plugin>
Plugin declaration enacts parent-defined behavior
Module specifies artifacts to deploy
Module specification is not required if deployment is self (i.e., the primary EJB/WAR/EAR produced by this module)
Figure 92.10. WAR/EAR Module Cleanup Declarations
<profile>
<id>undeploy</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<executions>
<execution>
<id>undeploy-ear</id>
<phase>pre-clean</phase>
<goals>
<goal>undeploy</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
Used to simply undeploy module from server
Requires artifact to be present
Profile keeps behavior from running under conditions that would fail build
Figure 92.11. Parent Definition
<pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<version>${cargo-maven2-plugin.version}</version>
<configuration>
<container>
<containerId>${cargo.containerId}</containerId>
<type>remote</type>
<log>target/server.log</log>
<output>target/output.log</output>
</container>
<configuration>
<type>runtime</type>
<properties>
<cargo.hostname>${jboss.mgmt.host}</cargo.hostname>
<cargo.jboss.management.port>${jboss.mgmt.port}</cargo.jboss.management.port>
</properties>
</configuration>
</configuration>
<dependencies>
<dependency>
<groupId>org.wildfly</groupId>
<artifactId>wildfly-controller-client</artifactId>
<version>${wildfly.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>cargo-prep</id>
<phase>pre-integration-test</phase>
<goals>
<goal>redeploy</goal>
</goals>
</execution>
<execution>
<id>cargo-post</id>
<phase>post-integration-test</phase>
<goals>
<goal>undeploy</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
Parent defines boiler-plate portion
Child will provide module-specific information
Figure 92.12. Deploy Application
$ mvn pre-integration-test -DskipTests ... [INFO] [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ ejb-basic-test --- ... [INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ ejb-basic-test --- ... [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ ejb-basic-test --- ... [INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ ejb-basic-test --- ... [INFO] --- maven-surefire-plugin:2.17:test (default-test) @ ejb-basic-test --- ... [INFO] --- maven-jar-plugin:2.5:jar (default-jar) @ ejb-basic-test --- ... [INFO] --- cargo-maven2-plugin:1.4.3:redeploy (cargo-prep) @ ejb-basic-test --- Oct 01, 2014 2:20:21 AM org.xnio.Xnio <clinit> INFO: XNIO version 3.2.2.Final Oct 01, 2014 2:20:21 AM org.xnio.nio.NioXnio <clinit> INFO: XNIO NIO Implementation Version 3.2.2.Final Oct 01, 2014 2:20:21 AM org.jboss.remoting3.EndpointImpl <clinit> INFO: JBoss Remoting version 4.0.3.Final [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS
Builds a new packaged, deployable artifact
Deploys and leaves on server
(optional)-DskipTests
bypasses any unit tests
Notice that build completes immediately after deploy with no IT tests or undeploy
Figure 92.13. Undeploy Application
$ mvn clean -Pundeploy ... [INFO] ------------------------------------------------------------------------ [INFO] Building EJB::Basic EJB::EAR 4.0.0-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] [INFO] --- cargo-maven2-plugin:1.4.3:undeploy (undeploy-ear) @ ejb-basic-ear --- Oct 01, 2014 2:23:50 AM org.xnio.Xnio <clinit> INFO: XNIO version 3.2.2.Final Oct 01, 2014 2:23:50 AM org.xnio.nio.NioXnio <clinit> INFO: XNIO NIO Implementation Version 3.2.2.Final Oct 01, 2014 2:23:50 AM org.jboss.remoting3.EndpointImpl <clinit> INFO: JBoss Remoting version 4.0.3.Final [INFO] [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ ejb-basic-ear --- [INFO] Deleting /home/jcstaff/workspaces/ejava-class/ejava-student/ejb/ejb-intro/ejb-basic-example/ejb-basic-ear/target
Undeploys atifact from server
Useful in automating cleanup
-Pundeploy
activates latched profile
Runs integration (IT) tests
Must be declared, not configured in by default
Runs after deployer completes and prior to undeployer
Figure 92.14. IT Module Declaration
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<cargo.startstop>${cargo-startstop}</cargo.startstop>
</systemPropertyVariables>
</configuration>
</plugin>
Plugin definition causes IT tests to run
Configuration defines environment for JVM
Figure 92.15. Optional Includes/Excludes
<configuration>
<includes>
<include>**/GreeterIT.java</include>
</includes>
</configuration>
Can turn off problem or lengthy test(s)
Figure 92.16. Parent Definitition
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<configuration>
<argLine>${surefire.argLine}</argLine>
</configuration>
<executions>
<execution> <!-- run the tests here -->
<id>integration-tests</id>
<phase>integration-test</phase>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
<execution> <!-- delay failures to after undeploy -->
<id>verify</id>
<phase>verify</phase>
<goals>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
Defines version# and wires plugin into build phases when declared by child module
Figure 92.17. Run IT Tests
$ mvn clean verify
$ mvn clean verify
...
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ ejb-basic-test ---
...
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ ejb-basic-test ---
...
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ ejb-basic-test ---
...
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ ejb-basic-test ---
...
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ ejb-basic-test ---
...
[INFO] --- maven-surefire-plugin:2.17:test (default-test) @ ejb-basic-test ---
...
[INFO] --- maven-jar-plugin:2.5:jar (default-jar) @ ejb-basic-test ---
...
[INFO] --- cargo-maven2-plugin:1.4.3:redeploy (cargo-prep) @ ejb-basic-test ---
Oct 01, 2014 2:28:02 AM org.xnio.Xnio <clinit>
INFO: XNIO version 3.2.2.Final
Oct 01, 2014 2:28:02 AM org.xnio.nio.NioXnio <clinit>
INFO: XNIO NIO Implementation Version 3.2.2.Final
Oct 01, 2014 2:28:02 AM org.jboss.remoting3.EndpointImpl <clinit>
INFO: JBoss Remoting version 4.0.3.Final
...
[INFO] --- maven-failsafe-plugin:2.17:integration-test (integration-tests) @ ejb-basic-test ---
-------------------------------------------------------
T E S T S
-------------------------------------------------------
...
Results :
Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
...
[INFO] --- cargo-maven2-plugin:1.4.3:undeploy (cargo-post) @ ejb-basic-test ---
...
[INFO] --- maven-failsafe-plugin:2.17:verify (verify) @ ejb-basic-test ---
...
-------------------------------------------------------
[INFO] BUILD SUCCESS
Runs all phases and stops just prior to installing into local repository
Cannot bypass unit tests (unless includes/excludes used)
Deploy artifacts to server
Run IT tests in IDE
Connect with Debugger
Figure 93.1. Deploy Artifacts
$mvn clean pre-integration-test ... 22:51:43,460 INFO [org.jboss.as.ejb3.deployment.processors.EjbJndiBindingsDeploymentUnitProcessor] (MSC service thread 1-2) JNDI bindings for session bean named GreeterEJB in deployment unit subdeployment "ejb-basic-ejb.jar" of deployment "ejb-basic-ear-4.0.0-SNAPSHOT.ear" are as follows: java:global/ejb-basic-ear/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:app/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:module/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:jboss/exported/ejb-basic-ear/ejb-basic-ejb/GreeterEJB!info.ejava.examples.ejb.basic.ejb.GreeterRemote java:global/ejb-basic-ear/ejb-basic-ejb/GreeterEJB java:app/ejb-basic-ejb/GreeterEJB java:module/GreeterEJB 22:51:43,563 INFO [org.jboss.weld.deployer] (MSC service thread 1-2) JBAS016005: Starting Services for CDI deployment: ejb-basic-ear-4.0.0-SNAPSHOT.ear
Deploy artifacts using Maven command from either module parent or IT test module
Build from parent will re-build, deploy, and leave deployed the artifacts
Can select a specific test or package of tests
Leverage reasonable hard-coded defaults and filtered resource files to make JUnit tests IDE friendly so that much of heavyweight Maven setup and teardown can be skipped when focusing on specific tests.
The following information is for remote debugging a standalone server. If you are using the IDE embedded mode, just re-start your server using the debug option and add source references as needed.
Figure 93.4. Enable Remote Debugging in Server
# wildfly-13.0.0.Final/bin/standalone.conf # Sample JPDA settings for remote socket debugging JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,address=8787,server=y,suspend=n"
--OR--
wildfly-13.0.0.Final$ bin/standalone.sh --debug 8787
Not required when using embedded server (just use debug-as)
Standalone server must be restarted after making this edit
Figure 93.6. Standalone Server Debugger Listen Port
========================================================================= Listening for transport dt_socket at address: 8787 ...
Debugger Client port must match what JBoss debugger listen port
You can optionally add modules now to resolve source code references
Execution will stop at server breakpoint
No source code will show up if not yet in search path
Add as many projects as you wish
You may have to stop and re-run your test for the source code path to take effect.
Notice we are seeing variables as well as line of execution
Copyright © 2019 jim stafford (jim.stafford@jhu.edu)
Built on: 2019-08-22 07:11 EST
Abstract
This presentation provides information for configuring EJBs with resources using deployment descriptors and JNDI Enterprise Naming Context (ENC). Thru one or more of these mechanisms you can inject resources -- such as Persistence Contexts, JMS Resources, references to other EJBs, etc -- into your EJB.
Table of Contents
Java Environment Naming Context (ENC)
Reading JNDI and ENC
Populating ENC using XML Deployment Descriptor
Populating ENC using Java Annotations
EJB Resource Injection
Using XML Deployment Descriptor Injection
Using Annotation Injection
Naming - provides a "white pages" lookup of an object based on name
Directory - provides a "yellow pages" lookup of an object based on properties
JavaEE providers required to supply Naming implementation
Naming consists of hierarchical Contexts (directories) and Objects
Reference to JNDI Naming tree through javax.naming.InitialContext
InitialContext jndi = null;
try {
jndi = new InitialContext();
...
Object object = jndi.lookup(jndiName);
} catch (NamingException ex) {
throw new EJBException(ex);
}
Lookups are passed a JNDI name
JNDI name have prefixes that can give them special meaning
(no prefix) - global namespace (available remotely)
java: - namespace available only within JVM
java:comp - an Enterprise Naming Context (ENC) relative to a specific component
ejb: - namespace for use by JBoss EJB Client
JavaEE resources are registered in the JNDI tree
Not all resources are available from remote clients
Resources in the global JNDI Naming Context are available to all clients
jms/RemoteConnectionFactory jms/queue/test jms/topic/test enc-config-example-ejb/XMLConfiguredEJB!ejava.ejb.examples.encconfig.ejb.JNDIReaderRemote
You can locate these global names using the java:/jboss/exported local context
java:jboss/exported/jms/RemoteConnectionFactory java:jboss/exported/jms/queue/test java:jboss/exported/jms/topic/test java:jboss/exported/enc-config-example-ejb/XMLConfiguredEJB!ejava.ejb.examples.encconfig.ejb.JNDIReaderRemote
Resources in the local JNDI Naming Context are available to components within the server
java:jboss/datasources/ExampleDS java:jboss/jaxr/ConnectionFactory java:jboss/mail/Default java:/ConnectionFactory java:/JmsXA java:/queue/test java:/topic/test
JavaEE 6/EJB3.1 added a portable JNDI naming standard for EJB that provides scoping for global, application, and module for the EJB's local and remote interfaces
java:global/enc-config-example-ejb/InjectedEJB!ejava.ejb.examples.encconfig.ejb.InjectedEJB java:app/enc-config-example-ejb/InjectedEJB!ejava.ejb.examples.encconfig.ejb.InjectedEJB java:module/InjectedEJB!ejava.ejb.examples.encconfig.ejb.InjectedEJB java:global/enc-config-example-ejb/InjectedEJB java:app/enc-config-example-ejb/InjectedEJB java:module/InjectedEJB
Component-specific namespace within JNDI tree
Holds references to values and objects in the environment
Theoretical equivalent to Unix soft links
ln -s <some physical file path> $HOME/<my logical path>
Accessible through "java:comp/env/(encname)" when using InitialContext
Context jndi = new InitialContext();
String jndiName = "java:comp/env/" + encname;
Object object = jndi.lookup(jndiName);
Accessible through "(encname)" when using injected SessionContext
private @Resource SessionContext ctx;
...
Object object = ctx.lookup(encname);
Populated into ENC using
The only reason to purposely populate the ENC is to support ENC lookups. Purposely populating the ENC is not necessary for alternate injection techniques.
Annotations
@Resource(lookup="java:jboss/datasources/ExampleDS", name="jdbc/ds2")
private DataSource ds2;
The annotation is looking up the potentially non-portable, local (java:) JNDI name from the global JNDI tree and making it available in the ENC (java:comp/env) in addition to injecting it into the Java variable. The later addition of the "lookup" annotation attribute (which may use a vendor-specific JNDI name) to injections in the EJB spec made configuring simple cases simple.
XML using META-INF/ejb-jar.xml
<resource-ref>
<res-ref-name>jdbc/ds2</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<lookup-name>java:jboss/datasources/ExampleDS</lookup-name>
</resource-ref>
The standard EJB deployment descriptor is looking up the potentially non-portable, local (java:) JNDI name from the global JNDI tree and making it available in the ENC (java:comp/env) The later addition of the "lookup-name" XML attribute (which may use a vendor-specific JNDI name) in the EJB spec eliminated the need for vendor-specific deployment descriptors for simple cases like this.
XML using META-INF/jboss-ejb3.xml
<resource-ref>
<res-ref-name>jdbc/ds3</res-ref-name>
<jndi-name>java:jboss/datasources/ExampleDS</jndi-name>
</resource-ref>
The vendor-specific EJB deployment descriptor is looking up the potentially non-portable, local (java:) JNDI name from the global JNDI tree and making it available in the ENC (java:comp/env) This is where potentially non-portable JNDI names had to be supplied in the original EJB specs.
Injected into EJB using
In these cases, we are purposely injecting the resource directly into the variable without a concern for the ENC.
Annotations without ENC
@Resource(lookup="java:jboss/datasources/ExampleDS")
private DataSource ds1;
Performing the injection into the Java field 100% within the code
Good for quick cases where developer knows the JNDI name of the resource and wants it injected into the variable
Code must be updated whenever the JNDI name of the resource changes
Annotations with populated ENC
@Resource(name="jdbc/ds3")
private DataSource ds3;
Performing the injection into the Java field using a portable ENC reference (java:comp/env/jdbc/ds3)
Adds a requirement for ENC to be populated
Code is portable and only ENC setup mechanism must be ported
XML using META-INF/ejb-jar.xml
<resource-ref>
<res-ref-name>jdbc/ds1</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<injection-target>
<injection-target-class>ejava.ejb.examples.encconfig.ejb.XMLConfiguredEJB</injection-target-class>
<injection-target-name>ds1</injection-target-name>
</injection-target>
<lookup-name>java:jboss/datasources/ExampleDS</lookup-name>
</resource-ref>
Populates the ENC (java:comp/env/jdbc/ds1) as before
Also performs injection into the EJB Java field using a standard XML deployment descriptor
EJB code is portable. Just the descriptor needs updating on change.
Injections are fairly basic. e.g., cannot inject into an object embedded within the EJB or define a global injection rule.
Perform actions on injected resources after injection
complete using @PostConstruct
@Resource(lookup="java:jboss/datasources/ExampleDS")
private DataSource ds1;
private SomeDao someDao;
@PostConstruct
public void init() {
//ds1 already injected -or-
ds1 = (DataSource)ctx.lookup("jdbc/ds1"); //-or-
ds1 = (DataSource)jndi.lookup("java:comp/env/jdbc/ds1"); //-or-
someDao = new SomeDaoImpl(ds1);
...
}
Call Order:
Properties are injected
@PostConstruct is invoked
This chapter focuses on injecting relevant resources into an EJB using modern injection techniques. From the points made in the previous chapter -- you know that there are more tedious and verbose techniques from the older EJB specs that pre-date @Annotations and ease-of-use enhancements. They will not be included here.
Inject a Resource to locate information from Container
private @Resource SessionContext ctx;
ctx.lookup("...")
ctx.getCallerPrincipal()
ctx.getRollbackOnly()
Inject a pool of database connections
From JNDI lookup using 100% annotation-based
@Resource(lookup="java:jboss/datasources/ExampleDS")
private DataSource ds1;
From ENC using annotations
@Resource(name="jdbc/ds3")
private DataSource ds3;
Inject access to the Persistence Unit
From persistence-unit name using 100% annotation-based
@PersistenceContext(unitName="enc-config")
private EntityManager em1;
@PersistenceUnit(unitName="enc-config")
private EntityManagerFactory emf1;
From ENC using annotations
@PersistenceContext(name="jpa/em3")
private EntityManager em3;
@PersistenceUnit(name="jpa/emf3")
private EntityManagerFactory emf3;
Use unitName
and not name
when specifying the name of the persistence unit. name
is for the ENC name.
Inject access to other EJBs
From type matching using 100% annotations
@EJB
private InjectedTypedLocal ejb9a;
@EJB
private InjectedTypedRemote ejb9b;
@EJB
private InjectedEJB ejb1;
Container is able to locate a single component that implements injected variable type
Last example is an example of a No Interface, @LocalBean EJB
No Interface is common technique for "helper" EJBs that implement a container-managed boundary (e.g., security @RunAs or transaction REQUIRES_NEW)
From type matching with Local/Remote ambiguities
@EJB
private InjectedTyped ejb10;
//DeploymentUnitProcessingException: WFLYEJB0406:
//No EJB found with interface of type 'ejava.ejb.examples.encconfig.ejb.InjectedTyped'
@EJB(beanInterface=InjectedTypedLocal.class)
private InjectedTyped ejb10a;
@EJB(beanInterface=InjectedTypedRemote.class)
private InjectedTyped ejb10b;
Example shows @Local and @Remote extending from common InjectedTyped interface that presents a problem trying to use the base interface type.
From JNDI lookup using 100% annotations
lookup is referencing a specific component using Portable JNDI Syntax
Global definition valid locally from outside the application. EAR may or may not apply.
java:global[/application name]/module name/enterprise bean name[/interface name]
@EJB(lookup="java:global/enc-config-example-ejb/InjectedEJB!ejava.ejb.examples.encconfig.ejb.InjectedEJB")
private InjectedEJB ejb3;
@EJB(lookup="java:global/enc-config-example-ejb/InjectedEJB")
private InjectedEJB ejb6;
Application definition valid locally from within the application
java:app[/module name]/enterprise bean name[/interface name]
@EJB(lookup="java:app/enc-config-example-ejb/InjectedEJB!ejava.ejb.examples.encconfig.ejb.InjectedEJB")
private InjectedEJB ejb4;
@EJB(lookup="java:app/enc-config-example-ejb/InjectedEJB")
private InjectedEJB ejb7;
Module definition valid locally with within same module java:module/enterprise bean name/[interface name]
java:module/enterprise bean name/[interface name]
@EJB(lookup="java:module/InjectedEJB!ejava.ejb.examples.encconfig.ejb.InjectedEJB")
private InjectedEJB ejb5;
@EJB(lookup="java:module/InjectedEJB")
private InjectedEJB ejb8;
From ENC using annotations
@EJB(name="ejb/ejb2")
private InjectedEJB ejb2;
JSR-299 (CDI 1.0), JSR-346 (CDI 1.1)
Started as "Web Beans"
Derived from "Seam"
Original draft conflicted with Google and Spring JSR submission
JSR-330 (Java Injection)
Approved version refactored to account for JSR-330
public class ProjectsDaoImpl implements ProjectsDao {
@Stateless
@Remote(TasksMgmtRemote.class)
public class TasksMgmtEJB implements TasksMgmtRemote {
Beans can be simple POJOs, EJBs, Servlets, etc.
CDI Beans generalize behavior provided by larger frameworks
@Inject
private ProjectsDao projectsDao;
Type-safe, Java-based container injection
Container will match injection point with managed bean of requested type
Ambiguity error if container finds multiple classes satisfying requested type
@Stateless
@Remote(JobsMgmtRemote.class)
public class JobsMgmtEJB implements JobsMgmtRemote {
@Inject
private JobsDao jobDao; i //<== injection target
public class JobsDaoImpl implements JobsDao { //<== candidate 1 a bean of type
public class CdiDemoConfig {
@Produces
public JobsDao jobsDao(@CdiDemo EntityManager em) { //<== candidate 2 - a producer of type
Caused by: org.jboss.weld.exceptions.DeploymentException: WELD-001409:
Ambiguous dependencies for type JobsDao with qualifiers @Default
at injection point [BackedAnnotatedField]
@Inject private ejava.examples.cdiconfig.ejb.JobsMgmtEJB.jobDao
at ejava.examples.cdiconfig.ejb.JobsMgmtEJB.jobDao(JobsMgmtEJB.java:0)
Possible dependencies:
- Managed Bean [class ejava.examples.cdiconfig.dao.JobsDaoImpl] with qualifiers [@Any @Default],
- Producer Method [JobsDao] with qualifiers [@Any @Default] declared as [[BackedAnnotatedMethod]
@Produces public ejava.examples.cdiconfig.CdiDemoConfig.jobsDao(@CdiDemo EntityManager)]
Two or more sources of the target injection type
Ambiguity can be resolved through use of @Alternatives, @Priority, and qualifiers
@Inject @CdiDemo
private JobsDao jobDao;
@Produces
@CdiDemo
JobsDao jobsDao(@CdiDemo EntityManager em) {
@Annotation qualifier specializes injection type to remove ambiguity
package ejava.examples.cdiconfig;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.*;
import static java.lang.annotation.ElementType.*;
import javax.inject.Qualifier;
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface CdiDemo {
}
java.lang.annoation
@Retention
how long the class is retained (default=CLASS)
RententionPolicy
CLASS - annotation recorded in class file but not available at runtime
RUNTIME - annotation recorded in class and available at runtime
SOURCE - annotation discarded by compiler
@Target
kind of Java construct the annotation applies to
ElementType
TYPE
FIELD
METHOD
PARAMETER
(more)
javax.inject (JSR-330)
@Inject
an injection target
@Named
string qualifier (for JSPs, etc. to reference bean using text)
@Qualifier
identifies qualifier annotations
javax.enterprise.inject (JSR-299)
@Produces
an injection source
@Any
no qualifiers
Instance<Bean>
runtime source of type Bean
Activated when beans.xml is located in appropriate location
META-INF/beans.xml for EJBs and regular JARs
WEB-INF/beans.xml for WARs
<?xml version="1.0" encoding="UTF-8"?>
<beans bean-discovery-mode="all" version="1.1"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd">
<scan>
<exclude name="fully qualified classname"/>
</scan>
<alternatives>
<class>...</class>
</alternatives>
</beans>
Can be blank or empty
Example shows XML alternative to using @Vetoed - which makes a bean unavailable for injection
Example shows enabling an @Alternative for application
public class CdiDemoConfig {
@Produces
@CdiDemo
@PersistenceContext(unitName="cdi-config")-->-+
+-<--------public EntityManager em; <--------------------+
|
| public class ProjectsDaoImpl implements ProjectsDao {
| private EntityManager em;
|
+--------> @PersistenceContext @CdiDemo
public void setEntityManager(EntityManager em) {
this.em = em;
}
Public field used as injection source
Useful when no behavior is required
@Produces
@CdiDemo
+-<------ public JobsDao jobsDao(@CdiDemo EntityManager em) {
| JobsDaoImpl impl = new JobsDaoImpl();
| impl.setEntityManager(em);
| return impl;
| }
|
| @Stateless
| @Remote(JobsMgmtRemote.class)
| public class JobsMgmtEJB implements JobsMgmtRemote {
| @Inject @CdiDemo
+----------> private JobsDao jobDao;
Injection type was not ambiguous - no @Qualifier required
Method called by container
Method declares parameters for what it needs to build product
Method parameters automatically injected
@Resource(lookup="java:jboss/datasources/ExampleDS")
@Produces
public DataSource ds;
Injection source populated from a JNDI lookup
@EJB(lookup="java:app/jndiDemoEJB/CookEJB!ejava.examples.jndidemo.ejb.CookLocal")
@Produces |
@Cook |
+---<--public Scheduler cook; <-------------------------+
|
| @Stateless
| public class TrainSchedulerEJB
| @Inject @Any
`----> protected Instance<Scheduler> anyCook;
protected Scheduler cook2; |
+---------------+
v
cook2 = anyCook.select(new CookQualifier()).get();
public class CookQualifier extends javax.enterprise.util.AnnotationLiteral<Cook>{}
Allows bean to hold onto all implementations of type
Bean can select from different implementations at runtime
@Named
public class JPASchedulerDAOImpl extends JPADAOBase<Task> implements SchedulerDAO {
Assignable to four (4) different types
JPASchedulerDAOImpl
JPADAOBase<Task>
SchedulerDAO
java.lang.Object
@Typed({SchedulerDAO.class, JPASchedulerDAOImpl.class})
public class JPASchedulerDAOImpl extends JPADAOBase<Task> implements SchedulerDAO {
@Type reduces number of types allowed
JPASchedulerDAOImpl
SchedulerDAO
@Named("sellerController")
@ConversationScoped
public class SellerController implements Serializable {
<h3>Sell Products (#{sellerController.seller.name})</h3>
Used for document expression languages to reference intended bean
Copyright © 2019 jim stafford (jim.stafford@jhu.edu)
Built on: 2019-08-22 07:11 EST
Abstract
This presentation provides coverage of how to package a JPA persistence unit within an EJB or WAR deployed to the server. It covers the resources involved and how they can be injected into the beans of the data tier as well as various issues that can arise and suggested solutions.
Table of Contents
Server-side Resources
PersistenceUnit/PersistenceContext injection
Managed Entities and Remote Interfaces
Persistence Context Propagation
Physical connections to database are placed into a pool
Logical connections are borrowed and returned from/to the pool
<subsystem xmlns="urn:jboss:domain:datasources:1.0">
<datasources>
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1</connection-url>
<driver>h2</driver>
<security>
<user-name>sa</user-name>
<password></password>
</security>
</datasource>
<drivers>
<driver name="h2" module="com.h2database.h2">
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
</driver>
</drivers>
</datasources>
</subsystem>
persistence.xml
Two transaction types -- one is primary path
No connection information - using pool from jta-data-source
Properties included define how to work with DB -- not how to connect
Likely the only transaction-type used
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">
<persistence-unit name="ejbjpa-hotel" transaction-type="JTA">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
...
<properties>
<property name="hibernate.dialect" value="${hibernate.dialect}"/>
...
</properties>
</persistence-unit>
</persistence>
No connection pooling
Propertites include connection information
Requires bean-managed transactions
@TransactionManagement(TransactionManagementType.BEAN)
Used for obscure situations - likely rarely used
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">
<persistence-unit name="ejbjpa-hotel-rl" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
...
<properties>
<!-- connection properties -->
<property name="javax.persistence.jdbc.url" value="${jdbc.url}"/>
<property name="javax.persistence.jdbc.driver" value="${jdbc.driver}"/>
<property name="javax.persistence.jdbc.user" value="${jdbc.user}"/>
<property name="javax.persistence.jdbc.password" value="${jdbc.password}"/>
<property name="hibernate.dialect" value="${hibernate.dialect}"/>
...
</properties>
</persistence-unit>
</persistence>
Use of transaction-type=RESOURCE_LOCAL bypasses a significant portion of what the application server provides (e.g., Connection Resource Pooling, Container Managed Transactions). This should only be used in limited cases and where the implementation requires. It should not be used because of lack of understanding of how to use the server-side JavaEE/EJB framework.
Deployment
META-INF/persistence.xml
Same location as normal JAR
Source
src/main/resources/META-INF/persistence.xml
enc-config-example-ejb |-- ejava | `-- ejb | `-- examples | `-- encconfig | |-- dto | | `-- NCPair.class | `-- ejb | |-- AnnotatedEJB.class | |-- InjectedEJB.class | |-- JNDIReader.class | |-- JNDIReaderRemote.class | `-- XMLConfiguredEJB.class `-- META-INF `-- persistence.xml
Deployment
WEB-INF/classes/META-INF/persistence.xml
Inside JARs within WEB-INF/lib also allowed
Source
src/main/resources/META-INF/persistence.xml
ejb-jpa-example-war |-- META-INF `-- WEB-INF |-- classes | |-- info | | `-- ejava | | `-- examples | | `-- ejb | | `-- ejbjpa | | |-- dto | | | |-- FloorDTO.class | | | `-- RoomDTO.class | | `-- ejb | | |-- HotelInitEJB.class | | |-- HotelInitRemote.class | | |-- HotelMgmtEJB.class | | |-- HotelMgmtLocal.class | | |-- HotelMgmtRemote.class | | |-- ReservationEJB.class | | `-- ReservationRemote.class | `-- META-INF | |-- ejb-jar.xml | |-- jboss-ejb3.xml | `-- persistence.xml `-- lib `-- ejb-jpa-example-blimpl-4.0.0-SNAPSHOT.jar
to have persistence.xml reference @Entity class(es) outside of local archive
jar-file element able to reference external JAR using a deterministic, portable path in EAR
ejbsessionBankEAR-4.0.0-SNAPSHOT |-- ejbsessionBankEJB.jar |-- ejbsessionBankWAR-4.0.0-SNAPSHOT.war |-- lib | |-- ejava-util-4.0.0-SNAPSHOT.jar | `-- ejbsessionBankImpl-4.0.0-SNAPSHOT.jar `-- META-INF `-- application.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">
<persistence-unit name="ejbsessionbank">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
<jar-file>lib/ejbsessionBankImpl-4.0.0-SNAPSHOT.jar</jar-file>
<properties>
<property name="hibernate.dialect" value="${hibernate.dialect}"/>
...
</properties>
</persistence-unit>
</persistence>
Can automatically locate Entities within WEB-INF classes
Cannot automatically locate Entities within WEB-INF/lib JARs
jar-file element not usable in WARs
ejb-jpa-example-war |-- META-INF `-- WEB-INF ... `-- lib `-- ejb-jpa-example-blimpl-5.0.0-SNAPSHOT.jar
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">
<persistence-unit name="ejbjpa-hotel">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
<!-- located in WEB-INF/lib/ejb-jpa-example-blimpl-${project.version}.jar -->
<class>info.ejava.examples.ejb.ejbjpa.bo.Guest</class>
<class>info.ejava.examples.ejb.ejbjpa.bo.Room</class>
<class>info.ejava.examples.ejb.ejbjpa.bo.Floor</class>
<properties>
<property name="hibernate.dialect" value="${hibernate.dialect}"/>
...
</properties>
</persistence-unit>
</persistence>
There is no need to declare "class" entity references into a properly configured "jar-file" that is located in the classpath of a deployed EAR. Use "class" entity references when using a WEB-INF/classes/META-INF deployment of a WAR and classes come from externally provided JAR -- or if you really need to only include specific entities in the archive.
@PersistenceContext
Injected with EntityManager
Transaction Scoped (default)
persistence context only sees a single Tx
container injects EntityManager with Tx active
@PersistenceContext(unitName="ejbjpa-hotel", type=PersistenceContextType.TRANSACTION)
private EntityManager em;
Extended Scope
persistence context may see multiple Tx
only relevant for Stateful EJBs
@PersistenceContext(unitName="ejbjpa-hotel", type=PersistenceContextType.EXTENDED)
private EntityManager em;
@PersistenceUnit
Injected with EntityManagerFactory
May be used to implement BEAN-managed transactions
@PersistenceContext.unitName is the name used within the persistence.xml
@PersistenceContext.name would be the ENC name normally defined in ejb-jar.xml
@Stateless
public class HotelMgmtEJB implements HotelMgmtRemote, HotelMgmtLocal {
@PersistenceContext(unitName="ejbjpa-hotel")
private EntityManager em;
private HotelDAO dao;
private HotelMgmt hotelMgmt;
@PostConstruct
public void init() {
dao = new JPAHotelDAO();
((JPAHotelDAO)dao).setEntityManager(em);
hotelMgmt = new HotelMgmtImpl();
((HotelMgmtImpl)hotelMgmt).setHotelDao(dao);
}
@PersistenceContext.synchronization
SynchronizationType.SYNCHRONIZED (default) -- EntityManager automatically joined with jtaTransaction
SynchronizationType.UNSYNCHRONIZED -- EntityManager manually joined with jtaTransaction. Useful for Stateful Session EJBs where interaction with EntityManager may span multiple client method calls.
@PersistenceContext.type
PersistenceContextType.TRANSACTION (default) - EntityManager transactions automatically managed by container and will invoke each root method once per transaction
PersistenceContextType.EXTENDED - EntityManager transactions may span multiple calls. Useful for Stateful Session EJBs to retain state during a complete session
Persistence unit (EntityManagerFactory) being injected from a JTA-managed source
i.e., the transaction must be managed at JTA level
BEAN-managed transactions means JTA transaction controlled thru injected UserTransaction
Method programmatically controlling scope of JTA transaction
em.joinTransaction() called on EntityManager created outside scope of JTA transaction
@Singleton
@Startup
@TransactionManagement(TransactionManagementType.BEAN)
public class HotelInitEJB implements HotelInitRemote {
@PersistenceUnit(unitName="ejbjpa-hotel")
private EntityManagerFactory emf;
@Resource
private UserTransaction tx;
@Override
public void businessMethod() {
EntityManager em=emf.createEntityManager();
HotelDAO dao = new JPAHotelDAO();
((JPAHotelDAO)dao).setEntityManager(em);
tx.begin();
em.joinTransaction(); //tells the EM to join the JTA Tx
...
tx.commit();
em.close();
}
}
Newer technique -- JavaEE's answer to Spring Configuration
Define qualifier annotation
package ejava.examples.jndidemo;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.*;
import static java.lang.annotation.ElementType.*;
import javax.inject.Qualifier;
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface JndiDemo {
}
Define producer of Persistence Context
import javax.enterprise.inject.Produces;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
public class SchedulerResources {
@PersistenceContext(unitName="jndidemo")
@Produces
@JndiDemo
public EntityManager em;
Define injection point
@Stateless
public class TrainSchedulerEJB
extends SchedulerBase implements TrainSchedulerRemote {
@Inject @JndiDemo
private EntityManager em;
The Qualifier annotation is only necessary when the deployed application contains multiple producers of an EntityManager.
Problem occurs when managed @Entity classes used as DTOs
Provider places proxy classes on managed entities to watch for changes
Provider classes can get marshaled back to client
Client encounters ClassNotFoundException when deserializing RMI object with provider class
javax.ejb.EJBException: java.lang.ClassNotFoundException: org.hibernate.proxy.pojo.javassist.SerializableProxy
Client must add hibernate-core in classpath to avoid ClassNotFoundException
<!-- used if hibernate entities re-used as DTOs -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<scope>test</scope>
</dependency>
@Entity
public class Room implements Serializable {
@Id
@Column(name="ROOM_NUMBER")
private int number;
@ManyToOne(optional=false, fetch=FetchType.LAZY)
@JoinColumn(name="FLOOR_ID")
private Floor floor;
@OneToOne(optional=true, fetch=FetchType.LAZY)
@JoinColumn(name="OCCUPANT_ID")
private Guest occupant;
Room has mandatory reference to Floor and optional reference to Guest
fetch=LAZY references most likely will be proxies implemented by JPA provider classes
@Override
public List<Room> getAvailableRooms(Integer level, int offset, int limit) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Room> qdef = cb.createQuery(Room.class);
Root<Room> r = qdef.from(Room.class);
...
em.createQuery(qdef)
.setFirstResult(offset);
.setMaxResults(limit);
.getResultList();
}
Room entities returned from query are passed to client
72 List<Room> rooms = hotelMgmt.getAvailableRooms(null, 0, 1);
75 Room room = rooms.get(0);
77 logger.info("what floor class is this??? {}", room.getFloor().getClass());
78 assertFalse("floor was not proxy", room.getFloor().getClass().equals(Floor.class));
80 Guest guest = new Guest("Cosmo Kramer");
81 guest = hotelMgmt.checkIn(guest, room);
82 logger.info("final guest: {}:{}", guest.getClass(), guest);
HotelMgmtEJBIT:77 - what floor class is this??? class info.ejava.examples.ejb.ejbjpa.bo.Floor_$$_jvst9a8_0 HotelMgmtEJBIT:82 - final guest: class info.ejava.examples.ejb.ejbjpa.bo.Guest:Guest [id=11, name=Cosmo Kramer]
Room returned with proxy class (Floor_$$_jvst9a8_0
)between Room and Floor
Requires client to have hibernate-core in classpath
Allows @Entity classes to be used as pure POJOs -- these have never been managed
Contains no provider proxy classes
No requirement to have client update classpath
@Override
public List<Room> getCleanAvailableRooms(Integer level, int offset, int limit) {
List<Room> rooms = getAvailableRooms(level, offset, limit);
return toClean(rooms);
}
/**
* This helper method will instantiate new entity classes to re-use as DTOs.
* This is done to remove hibernate-proxy classes that are part of the managed
* entity.
*/
private List<Room> toClean(List<Room> rooms) {
if (rooms==null) { return null; }
List<Room> cleanRooms = new ArrayList<Room>(rooms.size());
for (Room room : rooms) {
Floor floor = room.getFloor();
Floor cleanFloor = new Floor(floor.getLevel());
Room cleanRoom = new Room(cleanFloor, room.getNumber());
cleanFloor.withRoom(cleanRoom);
Guest occupant = room.getOccupant();
if (occupant!=null) {
Guest cleanOccupant = new Guest(occupant.getId());
cleanOccupant.setName(occupant.getName());
cleanRoom.setOccupant(cleanOccupant);
}
cleanRooms.add(cleanRoom);
}
return cleanRooms;
}
EJB Remote facade creating new instances of @Entity classes
this looks like a good floor: class info.ejava.examples.ejb.ejbjpa.bo.Floor final guest: class info.ejava.examples.ejb.ejbjpa.bo.Guest:Guest [id=17, name=Cosmo Kramer]
Client now gets Room.floor without provider proxy class in between
The "cleansing" method was shown as a helper method for simplicity. It is advised to encapsulate that within a separate helper class so that it is easily used by other Remote Facades that must cleans their returned objects.
Caller attempts to access an Entity property that has not yet been loaded from the database
Provider unable to fullfil that request
DB session closed before necessary references resolved
Common when using @Entities across Persistence Unit boundaries
Not unique to RMI
@Entity
public class Floor implements Serializable {
@Id
@Column(name="LEVEL", nullable=false)
int level;
@OneToMany(mappedBy="floor",
fetch=FetchType.LAZY,
cascade={CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.DETACH},
orphanRemoval=true)
@OrderBy("number")
List<Room> rooms;
Rooms fetch=LAZY
Rooms must be fetched within same DB session when using that collection
@Stateless
public class HotelMgmtEJB implements HotelMgmtRemote, HotelMgmtLocal {
...
@Override
public Floor getFloor(int level) {
return em.find(Floor.class, level);
}
Floor being marshaled directly back to client without addressing LAZY fetches
112 Floor floor = hotelMgmt.getFloor(0);
113 assertNotNull("floor not found", floor);
114 try {
115 logger.info("foor has {} rooms", floor.getRooms().size());
116 fail("did not get lazy-load exception");
117 } catch (LazyInitializationException expected) {
118 logger.info("got expected exception:{}", expected.toString());
119 }
HotelMgmtEJBIT:118 - got expected exception:org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: info.ejava.examples.ejb.ejbjpa.bo.Floor.rooms, could not initialize proxy - no Session
Floor can be accessed
Floor.room access causes Lazy-load exception of collection
Must be done in location where Persistence Unit still accessible
Simple/brute force technique to stimulate resolution of references
Where do you stop???
Repetitive round trips to DB can be expensive
@Override
public Floor getTouchedFloor(int level) {
Floor floor = getFloor(level);
if (floor!=null) {
//touch the managed-floor to cause lazy-loads to be resolved
floor.getRooms().isEmpty();
for (Room room: floor.getRooms()) {
Guest guest = room.getOccupant();
if (guest!=null) {
guest.getName(); //touch all occupants to cause lazy-loads to be resolved
}
}
}
return floor;
}
Server-side code, within the DB session boundary stimulates references to be loaded prior to marshaling back to client
Floor floor = getFloor(level);
select floor0_.LEVEL as LEVEL1_0_0_ from EJBJPA_FLOOR floor0_ where floor0_.LEVEL=?
if (floor!=null) {
floor.getRooms().isEmpty();
select rooms0_.FLOOR_ID as FLOOR_ID2_0_0_, rooms0_.ROOM_NUMBER as ROOM_NUM1_2_0_, rooms0_.ROOM_NUMBER as ROOM_NUM1_2_1_, rooms0_.FLOOR_ID as FLOOR_ID2_2_1_, rooms0_.occupant_GUEST_ID as occupant3_2_1_ from EJBJPA_ROOM rooms0_ where rooms0_.FLOOR_ID=? order by rooms0_.ROOM_NUMBER
for (Room room: floor.getRooms()) { Guest guest = room.getOccupant(); if (guest!=null) { guest.getName(); //touch all occupants to cause lazy-loads to be resolved
select guest0_.GUEST_ID as GUEST_ID1_1_0_, guest0_.name as name2_1_0_ from EJBJPA_GUEST guest0_ where guest0_.GUEST_ID=? select guest0_.GUEST_ID as GUEST_ID1_1_0_, guest0_.name as name2_1_0_ from EJBJPA_GUEST guest0_ where guest0_.GUEST_ID=?
DAO queries crafted to support remote access patterns
LAZY fetches resolved through "join fetch" queries
@Override
public Floor getFetchedFloor(int level) {
List<Floor> floors = em.createNamedQuery("Floor.fetchFloor",
Floor.class)
.setParameter("level", level)
.getResultList();
return floors.isEmpty() ? null : floors.get(0);
}
@Entity
@NamedQueries({
@NamedQuery(name="Floor.fetchFloor",
query="select f from Floor f "
+ "join fetch f.rooms r "
+ "join fetch r.occupant "
+ "where f.level=:level")
})
public class Floor implements Serializable {
Join fetch used to EAGER-ly load child rows
Less trips to DB for fatch=LAZY mappings
select floor0_.LEVEL as LEVEL1_0_0_, rooms1_.ROOM_NUMBER as ROOM_NUM1_2_1_, guest2_.GUEST_ID as GUEST_ID1_1_2_, rooms1_.FLOOR_ID as FLOOR_ID2_2_1_, rooms1_.occupant_GUEST_ID as occupant3_2_1_, rooms1_.FLOOR_ID as FLOOR_ID2_0_0__, rooms1_.ROOM_NUMBER as ROOM_NUM1_2_0__, guest2_.name as name2_1_2_ from EJBJPA_FLOOR floor0_ inner join EJBJPA_ROOM rooms1_ on floor0_.LEVEL=rooms1_.FLOOR_ID inner join EJBJPA_GUEST guest2_ on rooms1_.occupant_GUEST_ID=guest2_.GUEST_ID where floor0_.LEVEL=? order by rooms1_.ROOM_NUMBER
Still have to know "when is enough -- enough"
Server-side has job to implement overall capability
Client may have an "outsider" role
@Entity
public class Room implements Serializable {
@Id
@Column(name="ROOM_NUMBER")
private int number;
...
@OneToOne(optional=true, fetch=FetchType.LAZY)
@JoinColumn(name="OCCUPANT_ID")
private Guest occupant;
More information than the client wants/needs
More information than what client should have
But that is how out server-side model is designed...
//lets see if we can manually find a vacant room.....
Floor floor = hotelMgmt.getFetchedFloor(0);
//all floors have at least one occupant
for (Room room: floor.getRooms()) {
Guest occupant = room.getOccupant();
if (occupant!=null) {
logger.info("hey {}, are you done with room {} yet?",
occupant.getName(), room.getNumber());
//that is just rude
}
}
hey guest 1, are you done with room 0 yet? hey guest 3, are you done with room 2 yet?
Client only needed to know if room was occupied -- not by who
public class RoomDTO implements Serializable {
private int number;
private boolean occupied;
Room DTO class is only expressing that room is occupied
@Override
public FloorDTO getFetchedFloorDTO(int level) {
Floor floor = getFetchedFloor(level);
return toDTO(floor);
}
private FloorDTO toDTO(Floor floor) {
if (floor==null) { return null; }
FloorDTO floorDTO = new FloorDTO(floor.getLevel());
if (floor.getRooms()!=null) { for (Room room: floor.getRooms()) {
floorDTO.withRoom(toDTO(room));
}}
return floorDTO;
}
private RoomDTO toDTO(Room room) {
if (room==null) { return null; }
RoomDTO roomDTO = new RoomDTO(room.getNumber());
//remote client shouldn't care who is in the room -- just if busy
roomDTO.setOccupied(room.getOccupant()!=null);
return roomDTO;
}
Similar to @Entity cleansing since DTO classes aren't managed
Can indirectly solve LAZY-load issue because @Entity is walked on server-side
Must still pay attention to DB access for performance reasons
//lets see if we can manually find a vacant room.....
FloorDTO floor = hotelMgmt.getFetchedFloorDTO(0);
//all floors have at least one occupant
for (RoomDTO room: floor.getRooms()) {
if (room.isOccupied()) {
logger.info("hey whoever, are you done with room {} yet?", room.getNumber());
//still rude, but a bit more private
}
}
hey whoever, are you done with room 0 yet? hey whoever, are you done with room 2 yet?
Client no longer has access to who is in each room -- but server-side does
The "toDTO" method was shown as a helper method for simplicity. It is advised to encapsulate that within a separate helper class so that it is easily used by other Remote Facades that must translate an Entity class to a DTO. Note that the reverse is also common -- to have DTOs converted to Entity POJOs for incoming objects.
EJB will instantiate persistence context if does not yet exist
Stateless EJBs may only have transaction-scope persistence contexts
Stateful EJBs may have transaction-scope or extended persistence contexts
EJBs can share persistence contexts
Stateless EJB can propagate its tx-scope persistence context to a called EJB
Stateful EJB can propagate its tx-scoped or extended persistence context to a called EJB
Stateless EJB can work with extended persistence context provided by upstream Stateful client
Stateful EJB cannot transform propagated tx-scope persistence context into an extended
EJB Facade can act as sharing point for common persistence context
Each interaction is independent of the next except for what is stored in DB and at client
@Override
public Guest checkIn(Guest guest, Room room) throws RoomUnavailableExcepton {
Room hotelRoom = dao.getRoom(room.getNumber());
dao.addGuest(guest);
hotelRoom.setOccupant(guest);
return guest;
}
(Error checking removed)
Locate specific room
Add guest to DB
Associate room with guest
List<Room> availableRooms = hotelMgmt.getAvailableRooms(null, 0, 0);
logger.debug("we have {} available rooms", availableRooms.size());
List<Guest> members = new ArrayList<Guest>(availableRooms.size());
int i=0;
for (Room room: availableRooms) {
Guest member = new Guest("member " + i++);
member = hotelMgmt.checkIn(member, room);
members.add(member);
}
Client performs actions one at a time
client: hotelMgmt.getAvailableRooms(null, 0, 0);
[HotelMgmtEJB] *** HotelMgmtEJB(1543684701):init ***
[stdout] Hibernate:
[stdout] select
[stdout] room0_.ROOM_NUMBER as ROOM_NUM1_2_,
[stdout] room0_.FLOOR_ID as FLOOR_ID2_2_,
[stdout] room0_.OCCUPANT_ID as OCCUPANT3_2_
[stdout] from
[stdout] EJBJPA_ROOM room0_
[stdout] where
[stdout] room0_.OCCUPANT_ID is null
[stdout] order by
[stdout] room0_.ROOM_NUMBER asc
[HotelMgmtEJB] *** HotelMgmtEJB(1543684701):destroy ***
Persistence context created
Available rooms loaded into persistence context
Result is returned
Persistence context destroyed
Room hotelRoom = dao.getRoom(room.getNumber());
[HotelMgmtEJB] *** HotelMgmtEJB(613346935):init ***
[HotelMgmtEJB] checkin(guest=Guest [id=0, name=member 0], room=Room [number=1, occupant=null])
[stdout] Hibernate:
[stdout] select
[stdout] room0_.ROOM_NUMBER as ROOM_NUM1_2_0_,
[stdout] room0_.FLOOR_ID as FLOOR_ID2_2_0_,
[stdout] room0_.OCCUPANT_ID as OCCUPANT3_2_0_
[stdout] from
[stdout] EJBJPA_ROOM room0_
[stdout] where
[stdout] room0_.ROOM_NUMBER=?
Persistence context created
Specific room loaded into persistence context
Guest inserted into DB
This listing provides the main take-away from the stateless solution. The hotel room(s) must be queried for from the database each time they are accessed from the database.
All rooms were queried for when looking for avaiable rooms
Individual room was queried for when making reservation
dao.addGuest(guest);
[stdout] Hibernate:
[stdout] call next value for hibernate_sequence
[HotelMgmtEJB] *** HotelMgmtEJB(613346935):destroy ***
[stdout] Hibernate:
[stdout] insert
[stdout] into
[stdout] EJBJPA_GUEST
[stdout] (name, GUEST_ID)
[stdout] values
[stdout] (?, ?)
Guest inserted into DB
The transaction does not end until exiting the Stateless EJB method. That means any DB constraint violations will not occur within the context of the EJB call that caused it unless you call em.flush() (as a form of debug) prior to exiting the business method.
hotelRoom.setOccupant(guest);
[stdout] Hibernate:
[stdout] update
[stdout] EJBJPA_ROOM
[stdout] set
[stdout] FLOOR_ID=?,
[stdout] OCCUPANT_ID=?
[stdout] where
[stdout] ROOM_NUMBER=?
Room.occupant foreign key updated
Guest returned to client
Persistence context destroyed
State can be cached in-memory on server-side
Facade EJB propagates persistence context to called EJBs
@Stateful
public class ReservationEJB implements ReservationRemote {
@PersistenceContext(unitName="ejbjpa-hotel", type=PersistenceContextType.EXTENDED)
private EntityManager em;
List<Guest> guests = new LinkedList<Guest>();
@Override
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public int addGuest(Guest guest) {
logger.debug("addGuest={}", guest);
if (guest!=null) {
guests.add(guest);
em.persist(guest); //<== no transaction active yet
}
return guests.size();
}
Stateful EJB injects @PersistenceContext -- propagated in downstream EJB calls
Extended persistence context -- may be associated with zero or more transactions
Performing in-memory actions outside of transaction boundary
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRED)
@Remove
public List<Guest> reserveRooms() throws RoomUnavailableExcepton {
List<Room> rooms = hotelMgmt.getAvailableRooms(null, 0, guests.size());
//assign each one of them a room
em.flush(); //<== flush guests persisted outside of a TX for demonstration
List<Guest> completed = new ArrayList<Guest>(guests.size());
Iterator<Room> roomItr = rooms.iterator();
for (Guest guest: guests) {
Room room = roomItr.next();
try {
guest = hotelMgmt.checkIn(guest, room); //<== will attempt to also persist guest
completed.add(guest);
} catch (RoomUnavailableExcepton ex) {
//rollback any previous reservations
ctx.setRollbackOnly();
throw ex;
}
}
return completed;
}
Method executed within active JTA transaction with persistence context associated with transaction
Guests already managed but will be fully persisted in this method
Calls to Stateless HotelMgmtEJB executed on this EJB's persistence context
@Remove indicates stateful instance to be discarded after method called
int availableRooms = hotelMgmt.getAvailableRooms(null, 0, 0).size();
logger.debug("we have {} available rooms", availableRooms);
ReservationRemote checkin = (ReservationRemote) jndi.lookup(reservationJNDI);
for (int i=0; i<availableRooms; i++) {
Guest member = new Guest("member " + i);
int count=checkin.addGuest(member); //this only modifies the in-memory persistence unit
logger.debug("we have {} in our group so far", count);
}
List<Guest> guests = checkin.reserveRooms(); //this is where the DB work gets committed
Multiple requests are issued to Stateful EJB
Specific method(s) act on that state
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
...
em.persist(guest); //<== no transaction active yet
[ReservationEJB] *** ReservationEJB(12230192):init ***
[ReservationEJB] addGuest=Guest [id=0, name=member 0]
[stdout] Hibernate:
[stdout] call next value for hibernate_sequence
[ReservationEJB] addGuest=Guest [id=0, name=member 1]
[stdout] Hibernate:
[stdout] call next value for hibernate_sequence
[ReservationEJB] addGuest=Guest [id=0, name=member 2]
[stdout] Hibernate:
[stdout] call next value for hibernate_sequence
[ReservationEJB] addGuest=Guest [id=0, name=member 3]
[stdout] Hibernate:
[stdout] call next value for hibernate_sequence
em.persist() outside of transaction causing causing sequence call but no table inserts
List<Room> rooms = hotelMgmt.getAvailableRooms(null, 0, guests.size());
[HotelMgmtEJB] *** HotelMgmtEJB(1162892707):init ***
[stdout] Hibernate:
[stdout] select
[stdout] room0_.ROOM_NUMBER as ROOM_NUM1_2_,
[stdout] room0_.FLOOR_ID as FLOOR_ID2_2_,
[stdout] room0_.OCCUPANT_ID as OCCUPANT3_2_
[stdout] from
[stdout] EJBJPA_ROOM room0_
[stdout] where
[stdout] room0_.OCCUPANT_ID is null
[stdout] order by
[stdout] room0_.ROOM_NUMBER asc limit ?
[HotelMgmtEJB] *** HotelMgmtEJB(1162892707):destroy ***
Downstream Stateless HotelMgmtEJB queried for rooms
Rooms loaded into persistence context
@TransactionAttribute(TransactionAttributeType.REQUIRED)
...
logger.debug("reserving {} rooms for {} guests", rooms.size(), guests.size());
em.flush(); //<== flush guests persisted outside of a TX for demonstration
[ReservationEJB] reserving 4 rooms for 4 guests
[stdout] Hibernate:
[stdout] insert
[stdout] into
[stdout] EJBJPA_GUEST
[stdout] (name, GUEST_ID)
[stdout] values
[stdout] (?, ?)
SQL inserts issued to DB during flush (requires transaction active)
guest = hotelMgmt.checkIn(guest, room); //<== will attempt to also persist guest
[HotelMgmtEJB] *** HotelMgmtEJB(1086999670):init ***
[HotelMgmtEJB] checkin(guest=Guest [id=65, name=member 0], room=Room [number=1, occupant=null])
[HotelMgmtEJB] *** HotelMgmtEJB(1086999670):destroy ***
[HotelMgmtEJB] *** HotelMgmtEJB(99312186):init ***
[HotelMgmtEJB] checkin(guest=Guest [id=66, name=member 1], room=Room [number=100, occupant=null])
[HotelMgmtEJB] *** HotelMgmtEJB(99312186):destroy ***
[HotelMgmtEJB] *** HotelMgmtEJB(545116383):init ***
[HotelMgmtEJB] checkin(guest=Guest [id=67, name=member 2], room=Room [number=102, occupant=null])
[HotelMgmtEJB] *** HotelMgmtEJB(545116383):destroy ***
[HotelMgmtEJB] *** HotelMgmtEJB(605810979):init ***
[HotelMgmtEJB] checkin(guest=Guest [id=68, name=member 3], room=Room [number=201, occupant=null])
[HotelMgmtEJB] *** HotelMgmtEJB(605810979):destroy ***
[stdout] Hibernate:
[stdout] update
[stdout] EJBJPA_ROOM
[stdout] set
[stdout] FLOOR_ID=?,
[stdout] OCCUPANT_ID=?
[stdout] where
[stdout] ROOM_NUMBER=?
[stdout] Hibernate:
...
[ReservationEJB] *** ReservationEJB(12230192):destroy ***
Stateless HotelMgmtEJB accesses Rooms from EntityManager first-level cache -- no additional DB access
Stateless HotelMgmtEJB updates Room.occupant FK to reference Guest -- who is also already managed and in first-level cache
JTA Transaction committed by container after Stateful EJB method exits
Stateful EJB and persistence context also destroyed after method exits
This listing provides the main take-away from the Stateful solution. The hotel room(s) are queried from up front, maintained in the EXTENDED_CONTEXT Persistence Context within the Stateful Session EJB, and accessed from that cache by the called Stateless Session EJB. This is the same Stateless Session EJB that accessed the DB each time when accessed without a caller cache.
All rooms were queried for when looking for avaiable rooms
Individual room accessed from cache when making reservation
Part of transaction ACID properties
Atomic
All or nothing
Consistent
Valid state
Isolation
Visibility of incomplete changes
Durable
Not forgotten once committed
Figure 101.2. Stateless EJB will Reject New Check-in when Occupied
@Override
public Guest checkIn(Guest guest, Room room) throws RoomUnavailableExcepton {
Room hotelRoom = dao.getRoom(room.getNumber());
if (hotelRoom==null) {
throw new RoomUnavailableExcepton(String.format("room [%d] does not exist", room.getNumber()));
}
if (hotelRoom.getOccupant()!=null) {
throw new RoomUnavailableExcepton(String.format("room is occupied by %s", hotelRoom.getOccupant()));
}
dao.addGuest(guest);
hotelRoom.setOccupant(guest);
return guest;
}
Rejects checkin when invalid
Completes check-in when valid
Transaction per call, no rolling back previous calls
Figure 101.3. Client uses up all Available Rooms
List<Guest> members = new ArrayList<Guest>(availableRooms.size());
int i=0;
for (Room room: availableRooms) {
Guest member = new Guest("member " + i++);
member = hotelMgmt.checkIn(member, room);
members.add(member);
}
Stateless EJB commits each of these check-ins
Figure 101.4. Client Attempts an Invalid Check-in
//try doing it again
Room room = availableRooms.get(0);
Guest member = new Guest("member " + i++);
try {
member = hotelMgmt.checkIn(member, room);
members.add(member);
fail("fail to detect bad checkin");
} catch (RoomUnavailableExcepton ex) {
logger.debug("expected exception making too many reservations:{}", ex.toString());
}
expected exception making too many reservations:info.ejava.examples.ejb.ejbjpa.bl.RoomUnavailableExcepton: room is occupied by Guest [id=103, name=member 0]
Additional check-in rejected -- nothing committed
Figure 101.5. Server still has Initial Committed Check-ins
logger.info("completed reservations for {} guests", members.size());
int availableRooms2 = hotelMgmt.getAvailableRooms(null, 0, 0).size();
logger.info("hotel has {} rooms available", availableRooms2);
assertEquals("", availableRooms.size()-members.size(), availableRooms2);
completed reservations for 4 guests hotel has 0 rooms available
Each check-in occured in own transaction
Later error did not impact previous completed transactions
Multiple resource actions part of same transaction
All-or-none performed
Figure 101.6. Client Issues Guests Up Front
ReservationRemote checkin = (ReservationRemote) jndi.lookup(reservationJNDI);
for (int i=0; i<availableRooms+1; i++) {
Guest member = new Guest("member " + i);
int count=checkin.addGuest(member);
logger.debug("we have {} in our group so far", count);
}
00:47:20 DEBUG HotelMgmtEJBIT:326 - we have 4 available rooms 00:47:20 DEBUG HotelMgmtEJBIT:332 - we have 1 in our group so far 00:47:20 DEBUG HotelMgmtEJBIT:332 - we have 2 in our group so far 00:47:20 DEBUG HotelMgmtEJBIT:332 - we have 3 in our group so far 00:47:20 DEBUG HotelMgmtEJBIT:332 - we have 4 in our group so far 00:47:20 DEBUG HotelMgmtEJBIT:332 - we have 5 in our group so far
Same stateful process as before -- but with one additional Guest (one too many)
Figure 101.7. Client Attempts to Check-in all Guests at Once
try {
checkin.reserveRooms();
fail("too many check-ins not detected");
} catch (RoomUnavailableExcepton ex) {
logger.debug("expected exception making too many reservations:{}", ex.toString());
}
Client attempts to check-in to all Rooms
Checked exception will be thrown
Figure 101.8. Stateful EJB Attempts to flush() Guests and Reserve all Rooms
@TransactionAttribute(TransactionAttributeType.REQUIRED)
...
em.flush(); //<== flush guests persisted outside of a TX for demonstration
[ReservationEJB] reserving 4 rooms for 5 guests
[stdout] Hibernate:
[stdout] insert
[stdout] into
[stdout] EJBJPA_GUEST
[stdout] (name, GUEST_ID)
[stdout] values
[stdout] (?, ?)
...
Five (5) Guests are flushed to database prior to the rollback
Figure 101.9. Stateful EJB rolls back Transaction on Stateless HotelMgmtEJB Exception
@Resource
private SessionContext ctx;
try {
guest = hotelMgmt.checkIn(guest, room); //<== will attempt to also persist guest
completed.add(guest);
} catch (RoomUnavailableExcepton ex) {
//rollback any previous reservations
ctx.setRollbackOnly();
throw ex;
}
[ReservationEJB] *** ReservationEJB(271512057):destroy ***
expected exception making too many reservations:info.ejava.examples.ejb.ejbjpa.bl.RoomUnavailableExcepton: found on 4 out of 5 required
Persisted Guests removed from database as a part of transaction rollback
Early check-ins never flushed to DB -- discarded as part of rollback
Figure 101.10. Client Checks and finds Rooms Still Available -- all Check-ins Rolled Back
int availableRooms2 = hotelMgmt.getAvailableRooms(null, 0, 0).size();
logger.info("hotel has {} rooms available", availableRooms2);
assertEquals("unexpected room count", availableRooms, availableRooms2);
hotel has 4 rooms available
All check-ins associated with Rooms removed as a part of rollback
One concurrency strategy
Assume failure and take steps to prevent
Create/wait-for a lock on data up-front, prior to getting the data
More expensive when failure assumption over estimated
Opposite of Optimistic Locking concurrency strategy
Assume success and account for failure
Attempt action and detect if action not performed
More efficient when failures do not occur
Some failures can be delegated back to client for resolution
Figure 101.11. Stateful Scenario Ontaining Pessamistic Lock with Rooms
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRED)
@Remove
public List<Guest> reserveRoomsPessimistic() throws RoomUnavailableExcepton {
List<Room> rooms = em.createQuery(...)
.setFirstResult(0)
.setMaxResults(guests.size())
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
//can also be set globally as persistence.xml property
.setHint("javax.persistence.lock.timeout", 5000)
.getResultList();
return reserveRooms(rooms);
}
[stdout] select [stdout] room0_.ROOM_NUMBER as ROOM_NUM1_2_, [stdout] room0_.FLOOR_ID as FLOOR_ID2_2_, [stdout] room0_.OCCUPANT_ID as OCCUPANT3_2_ [stdout] from [stdout] EJBJPA_ROOM room0_ [stdout] where [stdout] room0_.OCCUPANT_ID is null [stdout] order by [stdout] room0_.ROOM_NUMBER asc limit ? for update --notice the FOR UPDATE
setLockMode(LockModeType.PESSIMISTIC_WRITE) - locks row (or table) for remainder of transaction
select ... FOR UPDATE issued for query
competing client thread is blocked until end of transaction
Table of Contents
Understand the purpose of a transaction
Local Transactions
JTA Transactions
XA Transactions
Manage transaction scope
Manage transaction state
Monitor transactions
At the completion of this topic, the student shall
have more understanding of:
Transactions in General
Transaction Management
Container Managed
Bean Managed
Transaction Scope
be able to:
Define a Transaction Scope for a Container Managed EJB method
Propogate Transaction Scope across Container Managed EJB methods
Implement an EJB using Bean Managed Transactions
Implement a Stateful Session EJB with transaction callbacks
Unit of work that accesses one more more resources (usually databases)
set of one or more activities related to each other
must be completed together or not at all
ATM
Withdraw from one source
Deposit to another
Order System
Locate item
Charge account
Schedule shipment
Medical System
Identify medical state
dispense prescription
Atomic
All parts of the transaction must complete or nothing at all
Consistent
Resource (e.g., data in database) is in consistent state upon completion
State "makes sense"
Database constraints (primary keys, foreign keys, etc.) are satisfied
Isolated
Transaction executes without interference from outside the scope of the transaction
Can the transaction read state not yet committed by another transaction?
Can the transaction read state that did not exist prior to it starting?
Durable
Committed changes are not lost if system crashes after the commit completes
Rather than autocommit each command, a set of commands are executed within larger transaction
A failure of any of these commands or failure to commit -- will rollback all commands
Container will coordinate transaction with resource manager for database
Same as before except commands spread across multiple databases
Container will coordinate transaction with resource managers for all databases involved
Requires a distributed transaction -- transparent to commands executed
Even though JTA transactions allow for the forming of a distributed transaction across multiple resources -- that does not mean you should design for that goal. Local transactions (single resource) are much faster than distributed transactions (multiple resources) and should be the primary goal in transaction design.
Same as before except commands issued to different types of resources
Message taken off queue only if overall transaction succeeds
Changes occur to database only if overall transaction succeeds
Message added to topic only if overall transaction succeeds
Many possibilities...
Design JMS interfaces to handle duplicate messages if work has to be repeated
Design database interaction to detect repeated work
Form a local transaction with the JMS Server limited to receipt and send of message
Form a new/separate local transaction with database commands
Leverage the completion status of the database to commit the JMS transaction
Resource
The actual (e.g., database) target of a command with the transaction
Local Transaction
A transaction involving only a single resource
Distributed Transaction
A transaction involving two or more resources
Two-Phase Commit
A protocol used to implement a distributed transaction
Long-lived Transactions
Transaction-like construct that spans long lengths of time and usually uses compensating transactions for rollback
Compensating Transaction
A set of follow-up actions performed outside of the resource to return to a prior state when transaction rollback not available
Transaction Context Propagation
Sharing of the same transaction context across components
Deadlock
When two or more threads own locks within a resource that the other thread requires to complete their transaction
EJB requires support of the JTA APIs
JTA is the JavaEE interface specification between participants in a transaction
applications
resource managers
application server
EJB is primarily a transaction framework/platform for application code
Two styles of transaction demarcation
Container-Managed Transactions (declarative; default)
Bean Managed Transactions (programmatic)
Container and server handle all interaction with resources -- regardless of demarcation style
EJB transactions are flat -- not nested
Declarative interface for transactions
Declarative attributes tell container to execute a method
within a transaction (MANDATORY, REQUIRED, REQUIRES_NEW, SUPPORTS)
without a transaction (SUPPORTS, UNSUPPORTED, NEVER)
Figure 102.4. Container-Managed Transactions
@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
public class HotelMgmtEJB implements HotelMgmtRemote, HotelMgmtLocal {
...
@PersistenceContext(unitName="ejbjpa-hotel")
private EntityManager em;
private HotelDAO dao;
private HotelMgmt hotelMgmt;
@PostConstruct
public void init() {
dao = new JPAHotelDAO();
((JPAHotelDAO)dao).setEntityManager(em);
hotelMgmt = new HotelMgmtImpl();
((HotelMgmtImpl)hotelMgmt).setHotelDao(dao);
}
@Override
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public Room getRoom(int number) {
return hotelMgmt.getRoom(number);
}
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Guest checkIn(Guest guest, Room room) throws RoomUnavailableExcepton {
return hotelMgmt.checkIn(guest, room);
}
For Container Managed Transactions (CMT), the transaction management code is in the Container and *not* within the method body of the EJB.
Programmatic interface to transactions
Interactions through javax.transaction.UserTransaction
interface
Everything between utx.begin() and utx.commit() is within a single transaction
Figure 102.5.
@Singleton
@Startup
@TransactionManagement(TransactionManagementType.BEAN)
public class HotelInitEJB implements HotelInitRemote {
@PersistenceUnit(unitName="ejbjpa-hotel")
private EntityManagerFactory emf;
@Resource
private UserTransaction tx;
@Override
public void clearAll() {
EntityManager em=emf.createEntityManager();
try {
JPAHotelDAO dao = new JPAHotelDAO();
dao.setEntityManager(em);
tx.begin();
em.joinTransaction(); //tells the EM to join the JTA UTx we are managing
dao.clearAll();
tx.commit();
} catch (Exception ex) {
try {
tx.rollback();
} catch (Exception ex2) {
throw new EJBException(ex2);
}
} finally {
if (em!=null) { em.close(); }
}
}
For Bean Managed Transactions (BMT), the transaction management code is in the EJB method. This adds extra non-business code and even the simplest scenario begins to get complicated.
Declarative interface to transactions
Declarative attributes tell container to execute a method
within a transaction (MADATORY, REQUIRED, REQUIRES_NEW, SUPPORTS)
without a transaction (SUPPORTS, UNSUPPORTED, NEVER)
Container completes transaction protocol prior to marshaling return objects
Container must suspend any active transaction
Figure 103.1. Transaction Not Supported
@Override
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public Product createProductAndShipments_NotSupported(Product product, List<Shipment> shipments)
throws UnexpectedState {
When called with transaction active:
Caller transaction suspended during call
Caller transaction resumed after call
Caller transaction is not impacted by resource managers used during call
Caller transaction not passed to anything this method calls
Container must invoke method with transaction active
Figure 103.2. Transaction Required
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Product createProduct(Product product) {
When called with transaction active:
Caller transaction context passed to method (unless @Asynchronous)
Caller transaction context passed to anything this method calls
Resource manager(s) called by method are enlisted in caller's transaction context
When called without transaction active (or @Asynchronous):
New transaction started
Transaction committed after method returns
Container can invoke method with transaction either active or inactive
Figure 103.3. Transaction Supports
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public int getShipmentCount(int productId) {
When called with transaction active:
Caller transaction context passed to method (unless @Asynchronous)
When called without transaction active:
No transaction started
Container must invoke method with new transaction active
Figure 103.4.
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public Shipment createShipment_RequiresNew(Shipment shipment) {
New transaction started
Transaction committed after method returns and prior to returning result to client
When called with transaction active:
Caller transaction suspended during call
Caller transaction resumed after call
Caller transaction is not impacted by resource managers used during call
Caller transaction not passed to anything this method calls
Caller must already have transaction active
When called with transaction active:
Caller transaction context passed to method
When called without transaction active:
javax.ejb.EJBTransactionRequiredException
Caller must never invoke method with transaction active
When called with transaction active:
javax.ejb.EJBException
When called without transaction active:
Called method continues without transaction
Figure 103.8. @Asynchronous Method with REQUIRED
@Asynchronous
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void addQuantity(int productId, int quantity) {
EJB transactions cannot propagate across @Asynchronous methods
Use of REQUIRED in @Asynchronous method will act like REQUIRES_NEW
Methods
@PostConstruct
@PreDestroy
@PrePassivate (Stateful)
@PostActivate (Stateful)
Operate by default in an undefined transaction context
May be defined with REQUIRES_NEW or NOT_SUPPORTED for Stateful and Singleton
Any transactions created during a lifecycle method will not be a part of the client's transaction
Figure 103.9. EJB Lifecycle Methods with Container-Managed Transactions
@Singleton
@TransactionManagement(TransactionManagementType.CONTAINER)
public class WarehouseTxEJB {
@TransactionAttribute(TransactionAttributeType.REQUIRED)
@PostConstruct
public void init() {
logger.debug("*** {}:init({}) ***", getClass().getSimpleName(), super.hashCode());
updateBeanCount(1);
}
@TransactionAttribute(TransactionAttributeType.REQUIRED)
@PreDestroy
public void destroy() {
logger.debug("*** {}:destroy({}) ***", getClass().getSimpleName(), super.hashCode());
updateBeanCount(-1);
}
private void updateBeanCount(int value) {
String beanName = getClass().getSimpleName();
BeanCount count=em.find(BeanCount.class,
beanName,
LockModeType.PESSIMISTIC_WRITE);
if (count!=null) {
count.setCount(count.getCount()+value);
} else {
count = new BeanCount(beanName);
count.setCount(value);
em.persist(count);
}
logger.debug("updatedBeanCount({}) to {}", count.getName(), count.getCount());
}
Must be either
REQUIRED - message receipt integrated with JTA transaction
NOT_SUPPORTED - message receipt not part of JTA transaction
EJB cannot access transaction
EJB has no programatic means to commit transaction -- commit managed by container
Commits are declarative by method boundaries
EJBs may be divided into second helper class to provide fine-grain control of transaction commit
No Interface Stateless Session EJBs are good solution for Tx Helper classes
EJB can only mark the transaction for rollback -- rollback managed by container
Unchecked exceptions automatically mark the transaction for rollback
Figure 103.10. EJBContext
public interface javax.ejb.EJBContext {
...
javax.transaction.UserTransaction getUserTransaction() throws java.lang.IllegalStateException;
void setRollbackOnly() throws java.lang.IllegalStateException;
boolean getRollbackOnly() throws java.lang.IllegalStateException;
...
}
public interface SessionContext extends EJBContext {
@Resource
SessionContext ctx;
...
ctx.setRollbackOnly();
...
getUserTransaction()
Returned instance can use used to demarcate transactions
Restricted to EJBs using bean-managed transactions
getRollbackOnly()
Tests whether current transaction is going to be rolled back at the end
Restricted to EJBs using container-managed transactions
setRollbackOnly()
Allows EJBs to trigger the current transaction to be rolled back at the end
Restricted to EJBs using container-managed transactions
Figure 103.11. Example: Explicit Rollback thru EJB/SessionContext
@Resource
private SessionContext ctx;
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Product createProductAndRollback(Product product) {
//create a product in the database
Product p=beanA.createProduct(product);
beanA.flush(); //here just to highlight the impact of the rollback
ctx.setRollbackOnly();
return p; //return the product even thow we are going to roll it back
}
Resources created/modified within the transaction -- including resources operated on by called EJBs -- will be rolled back.
Figure 103.12. Example: IT Client Triggers Rollback
Product product = warehouse.createProductAndRollback(new Product("thing", 6));
//since we did not throw an exception -- we get a normal return object
assertEquals("unexpected quantity for product", 6, product.getQuantity());
//since we setRollbackOnly() prior to the return -- our product will not be there
Product p2 = warehouse.getProduct(product.getId());
assertNull("unexpected/rolled back product found", p2);
Even though the application did not throw an exception and gracefully returned a result -- the IT test shows the entity was not stored during the callback
Two types
Checked/Application
Unchecked/Runtime
Checked/Application exceptions, by default, do not rollback the transaction when throw
Unchecked/Runtime exceptions do rollback the transaction when thrown
No automatic rollback by default
Figure 103.13. Example: Checked/Business Exception
public class MyCheckedProductException extends Exception {
private Product product;
public MyCheckedProductException(Product product, String message) {
super(message);
this.product = product;
}
Checked exceptions, but default, do not trigger rollbacks
Figure 103.14. Example: EJB Method Commits Data, Throws Checked Exception
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Product createProductAndThrowChecked(Product product)
throws MyCheckedProductException {
//create a product in the database
Product p=beanA.createProduct(product);
//throw a checked exception -- the product will still be committed
throw new MyCheckedProductException(p, "planned checked exception");
}
An error in processing can be reported with an exception without rollback
Figure 103.15. Example: Client Triggers Exception and Verifies Data Committed
Product product = null;
try {
warehouse.createProductAndThrowChecked(new Product("thing", 6));
fail("planned exception not thrown");
} catch (MyCheckedProductException ex) {
//the *checked* exception contains the product in the exception
product = ex.getProduct();
assertEquals("unexpected quantity for product", 6, product.getQuantity());
}
//our product should exist
Product p2 = warehouse.getProduct(product.getId());
assertEquals("product not updated async", 6, p2.getQuantity());
IT test verifies entity thrown with the exception was persisted
Checked/Application exceptions can be configured to trigger a rollback
Figure 103.16. Example: Checked/Business Exception
import javax.ejb.ApplicationException;
@ApplicationException(rollback=true)
public class MyCheckedRollbackProductException extends Exception {
Annotation tells container to automatically rollback transaction if thrown
Figure 103.17. Example: EJB Method Throws Checked Exception to Automatically Rollback
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Product createProductAndThrowCheckedRollback(Product product) throws MyCheckedRollbackProductException {
//create a product in the database
Product p=beanA.createProduct(product);
beanA.flush(); //here just to highlight the impact of the rollback
//throw a checked exception marked for rollback -- the product will *NOT* be committed
throw new MyCheckedRollbackProductException(p, "planned checked exception");
}
An error in processing can be reported with an exception without rollback
Figure 103.18. Example: Client Triggers Exception and Verifies Data Committed
Product product = null;
try {
warehouse.createProductAndThrowCheckedRollback(new Product("thing", 6));
fail("planned exception not thrown");
} catch (MyCheckedRollbackProductException ex) {
//the *checked* exception contains the product in the exception
product = ex.getProduct();
assertEquals("unexpected quantity for product", 6, product.getQuantity());
}
//since we used a rollback=true checked exception -- our product will not be there
Product p2 = warehouse.getProduct(product.getId());
assertNull("unexpected/rolled back product found", p2);
IT test verifies entity thrown with the exception was persisted
Persistence contexts can also get propagated
Stateless EJB can propagate its tx-scope persistence context to a called EJB
Stateful EJB can propagate its tx-scoped or extended persistence context to a called EJB
Stateless EJB can work with extended persistence context provided by upstream Stateful client
Stateful EJB cannot transform propagated tx-scope persistence context into an extended
EJB Facade can act as sharing point for common persistence context
Programmatic interface to transactions
Interactions through javax.transaction.UserTransaction
interface
Everything between utx.begin() and utx.commit() is within a single transaction
Bean is in control
Figure 104.1. Example: EJB with Bean-Managed Transactions
@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class BmtCreateEJB {
//synchronization=SynchronizationType.SYNCHRONIZED is the default
@PersistenceContext(unitName="ejbtx-warehouse",
synchronization=SynchronizationType.SYNCHRONIZED)
private EntityManager em;
@Resource
private UserTransaction utx;
public Product createProduct(Product product) {
try {
txWatcher.watchTransaction(getClass(), super.hashCode());
utx.begin(); //<=== manually start Tx
em.joinTransaction(); //<=== only necessary with UNSYNCHRONIZED
em.persist(product);
utx.commit(); //<=== manually end Tx
logger.debug("createProduct()={}", product);
return product;
} catch (Exception ex) {
try { utx.rollback(); }
catch (Exception ex2) { ... }
throw new EJBException("error managing transaction", ex);
}
}
Can inject @PersistenceContext/EntityManager or @PersistenceUnit/EntityManagerFactory
EntityManager already SYNCHRONIZED with UserTransaction by default
EntityManager can be manually synchronized using em.joinTransaction()
Useful in Stateful Session EJBs were may interact with EntityManager over multiple methods
Figure 104.2. UserTransaction
public interface javax.transaction.UserTransaction {
void begin() throws javax.transaction.NotSupportedException,
javax.transaction.SystemException;
void commit() throws javax.transaction.RollbackException,
javax.transaction.HeuristicMixedException,
javax.transaction.HeuristicRollbackException,
java.lang.SecurityException,
java.lang.IllegalStateException,
javax.transaction.SystemException;
void rollback() throws java.lang.IllegalStateException,
java.lang.SecurityException,
javax.transaction.SystemException;
void setRollbackOnly() throws java.lang.IllegalStateException,
javax.transaction.SystemException;
int getStatus() throws javax.transaction.SystemException;
void setTransactionTimeout(int) throws javax.transaction.SystemException;
}
Current message being processed cannot be made to join current UserTransaction
Must use Container-Managed REQUIRED scope
Stateful session EJBs using container-managed transactions can receive transaction events
EJBs using bean-managed transactions are in control of their transaction and do not need events
Figure 105.1. Example: Stateful EJB with Transaction Callbacks
@Stateful public class TxWatcherEJB { ... /** * By calling this (or any other business method, the sessionEJB will be created * and enlisted in the current transaction */ public void watchTransaction(Class<?> clazz, int hashCode) { String txName = clazz.getSimpleName() + ":" + hashCode; if (this.txName==null || !this.txName.equals(txName)) { logger = LoggerFactory.getLogger(clazz); logger.debug("watcher EJB {} enlisted in transaction for {}", beanName, txName); this.txName = txName; } else { logger.debug("transaction continued for {}", this.txName); } } @AfterBegin public void afterBegin() { logger.debug("transaction for {} has started", beanName); } @BeforeCompletion public void beforeCompletion() { logger.debug("transaction for {} is committing", beanName); } @AfterCompletion public void afterCompletion(boolean committed) { logger.debug("transaction committed for {}, commited={}", beanName, committed); }
Implement a javax.ejb.SessionSynchronization
callback interface or use annotations
Annotate methods -- no more than once per annotation
afterBegin()/@AfterBegin
Executed within the transaction, prior to first business method
beforeCompletion()/@BeforeCompletion
Executed after last business method complete -- chance to rollback
afterCompletions(boolean)/@AfterCompletion
Executed after transaction complete with a boolean complete
providing status
Cannot be final or static
A stateful session EJB may participate in only a single transaction at a time
A stateful session EJB will not be passivated while in an active transaction
A stateful session EJB's conversational state is not transactional
i.e., it is not automatically rolled back
Copyright © 2019 jim stafford (jim.stafford@jhu.edu)
Built on: 2019-08-22 07:11 EST
Abstract
This presentation contains introductory topics for exposing and calling server-side behavior using REST-like Web Resource interfaces implemented with JAX-RS, JAXB, and JSONB.
Table of Contents
Expose EJB-based services as resources through Web Application Programming Interfaces (APIs)
Make client resource requests of these APIs
Exchange data payloads using common Internet exchange protocols (JSON and XML)
At the completion of this topic, the student shall
have more understanding of:
Building a Web Resource Facade
Building a Web Resource Client
How Web APIs align with "REST-like" concepts
Implementing XML and JSON DTO data exchanges
be able to:
Design and implement a Web Resource Facade using JAX-RS
Implement an Web Resource Client using JAX-RS
Design and implement a set of URIs, Methods, and status codes to expose EJB capability as a Web Resource using JAX-RS
Implement Java class marshaling and demarshaling using JAXB and JSONB (and Jackson)
Architectural Style for creating web services
Provide interoperability between computer systems on the Internet
Uses a uniform and predefined set of stateless operations
Defined in 2000 by Roy Fielding in his doctoral dissertation that was also used to design HTTP 1.1 [11]
REST officially contains much more than most interface designs use
Leads to some common questions
"What is your definition of REST?"
"How RESTful are you?"
Common "REST-like" architectures are based on a few main principals
HTTP Protocol
Resources
URIs
Standard HTTP Method Vocabulary
Helps leverage HTTP Internet caches
Standard Content Types
Links somewhat used
Rarely opaque and rarely discovered -- especially within payload body
Clients commonly construct URI links from ID responses and knowledge of API
Uncommon REST features adopted
Dynamic discovery of resource capabilities
Level 0 - Using HTTP soley as a transport
Level 1 - Using Resources
Level 2 - Using HTTP Methods
Level 3 - Using Hypermedia Controls
Previous commmunications protocols attempted to be transport agnostic
CORBA mapped to HTTP, JMS, etc.
Everything used an HTTP POST when mapped to HTTP protocol
Other HTTP verbs (e.g., GET, DELETE, PUT, HEAD) not used
Bypassed built-in HTTP capabilities (e.g., caching) of the Internet
REST technically does not exist outside of the HTTP transport protocol
Everything is expressed in HTTP-terms
REST APIs leverage the world's investment into Internet communications
An asset to be exposed to the web
Document
Set of properties
Action or Activity -- even verbs can be treated as nouns
(nearly anything)
Web has limited number of methods but can have an unlimited number of resources
Example Resources
products
categories
customers
todos
Resources can be nested
categories(id).products *
customers(id).purchases *
todos(name).items *
* Conceptual representation
An address (of varying detail) to access a particular resource
Technically, URIs can be either URNs or URLs
Uniform Resource Name [13]
Resource identity
Globally unique
Example:
urn:info.ejava.products:1 <core xmlns="urn:activemq:core">
Uniform Resource Locator [14]
Resource's location on a network
Contains protocol information -- "how to get it"
Example:
http://127.0.0.1/jaxrsInventoryWAR/api/products/1 https://127.0.0.1/jaxrsInventoryWAR/api/products/1 ftp://127.0.0.1/info.ejava.products:1
Commonly -- URIs are a partial URL
Commonly lack protocol and physical location
Relative to some point in the application
Example:
/jaxrsInventoryWAR/api/products/1 /api/products/1 products/1
Example resource collection URI
/api/products /api/categories /api/customers /api/todo_lists
Example individual resource URIs (with mandatory path {parameter}s
/api/products/{productId} /api/categories/{categoryId} /api/customers/{customerId} /api/customers/{customerId}/sales
Example nested resource URIs
/api/products/{productId}/instructions /api/categories/{categoryId}/products /api/customers/{customerId}/purchases /api/todo_lists/{listName}/todo_items
URIs may express variable parameters
Use query parameters for optional variables
http://127.0.0.1:8080/jaxrsInventoryWAR/api/categories?name=&offset=0&limit=0
Nested path parameters may express mandatory variables
http://127.0.0.1:8080/jaxrsInventoryWAR/api/products/{id} http://127.0.0.1:8080/jaxrsInventoryWAR/api/products/1 id=>1
URI naming conventions
Use a plural name for resource collections
/api/todo_lists
Use an ID below the plural resource collection to refer to a specific resource
/api/todo_lists/{listName}
Bounded Set
provides "uniform interface" across all resources
Primary Set of Methods
GET - non-destructive read
POST - create and other methods
PUT - create or update
DELETE - delete
Secondary Set of Methods
HEAD - a GET without the data - metadata only
OPTIONS - lists which methods supported
Example: Get Product ID=1
GET http://127.0.0.1:8080/jaxrsInventoryWAR/api/products/1
Does the method change the state of the resource?
Safe Methods
GET
HEAD
OPTIONS
Unsafe Methods
POST
PUT
DELETE
Internet communications is based upon the above method saftey expectations, but these are just definitions. You have the power to implement resource methods any way you wish. However, avoid violating these method expectations -- or your API will not be immediately understood and will render built-in Internet capabilities (e.g., caches) useless. The following are examples of what *not* to do:
GET /jaxrsInventoryWAR/api/products/1?command=DELETE POST /jaxrsInventoryWAR/api/products/1 content: {command:'getProduct'}
Design goal for each method implementation -- regardless of "safety"
Method will result in the same resource ending state no matter how many times executed
POST /api/todo_lists -- create a new and specific TodoList
PUT /api/todo_lists/bills -- update/replace existing TodoList
DELETE /api/todo_lists/bills
Idempotent methods
GET
PUT
DELETE
OPTIONS
HEAD
Non-Idempotent method(s)
POST
The standard convention of Internet protocol is that all methods except for POST are assumed to be idempotent. That means a page refresh for a page obtained from a GET gets immediately refreshed and a warning dialogue is displayed if it was the result of a POST.
Standardized
2xx - success
3xx - success/redirect
4xx - client error
5xx - server error
Common Set
200 - OK
"We achieved what you wanted and might have previously did this"
201 - CREATED
"We did what you asked and a new resource was created"
204 - NO_CONTENT
"Just like a 200 with an empty payload, except the status makes this clear"
400 - BAD_REQUEST
"Your request is invalid and will never be accepted -- stop it"
401 - UNAUTHORIZED
"We need to know who you are before we do this"
403 - FORBIDDEN
"We know who you are and you cannot say what you just said"
500 - INTERNAL_ERROR
"Ouch! Nothing wrong with what you asked for or supplied, but we currently have issues completing. Try again later and we may have this fixed."
REST-like applications likely stop at providing standard links in HTTP headers
POST http://localhost:8080/ejavaTodos/api/todo_lists {"name":"My First List"} =>Created/201 Location: http://localhost:8080/ejavaTodos/api/todo_lists/My%20First%20List Content-Location: http://localhost:8080/ejavaTodos/api/todo_lists/My%20First%20List
Content-Location - direct URL for the data returned in the payload
Location - a re-direct to a new URL
Result of a new resource being created
Terms "REST" and "RESTful" have a specific meaning defined by Roy Fielding
Very few APIs achieve full REST adoption -- but that is OK!!! -- just call it "REST-like" or "HTTP-based"
Most serious REST-like APIs adopt
HTTP
Multiple Resources
URIs
HTTP-compliant use of Methods
Safety
Idempotent
Minor use of opaque links
[11] "Architectural Styles and the Design of Network-based Software Architectures. Doctoral dissertation", Roy Thomas Fielding, University of California, Irvine, 2000 HTML Version
determine the baseUrl for the application
String baseHttpUrlString = System.getProperty("url.base.http", "http://localhost:8080");
build a URL to the specific resource
URI resourceUrl = javax.ws.rs.core.UriBuilder.fromUri(baseHttpUrlString)
//.path("") //using root resource - no need for extra path
.build();
construct a JAX-RS client
javax.ws.rs.client.Client client = javax.ws.rs.client.ClientBuilder.newClient();
create the WebTarget to represent intended resource
javax.ws.rs.client.WebTarget target = client.target(resourceUrl);
create an overall request, indicating Acceptable response types
javax.ws.rs.client.Invocation.Builder request = target.request(MediaType.TEXT_HTML_TYPE);
create a method-specific request
javax.ws.rs.client.Invocation method = request.buildGet();
invoke the request and get an actual response from remote resource
javax.ws.rs.core.Response response = method.invoke();
interpret response
javax.ws.rs.core.Response.StatusType status = response.getStatusInfo();
int statusCode = response.getStatus();
assertEquals("unexpected statusType", Status.OK, status);
assertEquals("unexpected statusCode", 200, statusCode);
get response entity
String content = response.readEntity(String.class);
assertNotEquals("unexpected size", 0, content.length());
logger.debug("GET {} => {}/{}", target.getUri(), response.getStatus(), response.getStatusInfo());
GET http://localhost:8080 => 200/OK
Declare root URI using class that extends Application
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("api")
public class TodosApplication extends Application {
}
Root will be accessible under (WAR context-root)/api
Declare resource class and its path below root
import javax.ws.rs.Path;
@Path("greetings")
public class GreetingsResource {
Resource will be accessible under (WAR context-root)/api/greetings
Declare resource method
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
@GET
@Path("hi")
@Produces(MediaType.TEXT_PLAIN)
public Response sayHi() {
//...
}
Be sure to declare the method as "public" - or else it will quietly fail
Resource will be accessible under (WAR context-root)/api/greetings/hi
Method will be accessible using GET /api/greetings/hi
# client-side
URI uri = UriBuilder.fromUri(baseTodosUrl)
.path("greetings")
.path("hi")
.build();
WebTarget target = client.target(uri);
Method will be used for all GET URI callers that can Accept "text/plain" content
# client-side
Response response = target.request(MediaType.TEXT_PLAIN_TYPE)
.get();
Implement a response
public Response sayHi() {
String entity = "hi";
ResponseBuilder rb = Response.ok(entity);
return rb.build();
}
Response will return a 200/OK status code
# client-side
String greeting = response.readEntity(String.class);
logger.info("GET {} => {}", target.getUri(), response.getStatusInfo());
# client-side GET http://localhost:8080/ejavaTodos/api/greetings/hi => OK
Response will return a text String using "text/plain" content marshaling
# client-side
String greeting = response.readEntity(String.class);
logger.info("{}", greeting);
# client-side hi
JAX-RS API
<properties>
<javax.ws.rs-api.version>2.1</javax.ws.rs-api.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<version>${javax.ws.rs-api.version}</version>
</dependency>
<dependencies>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<scope>provided</scope>
</dependency>
Client-Side Provider
<properties>
<resteasy.version>3.5.1.Final</resteasy.version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<scope>test</scope>
</dependency>
Additional dependencies will be required when adding structured content to payloads
Use Web API resource classes as HTTP facades
Inject EJB/business logic components to perform details of work
Report obvious status from EJB/business logic and error messages
Report accurate status and error messages from Web API
Define an EJB to handle the (transactional and non-transactional) business logic
# GreetingEJB.java
import javax.ejb.Stateless;
# GreetingEJB.java
@Stateless
public class GreetingEJB {
}
This EJB happens to be a No Interface EJB
The class is the interface
Inject EJB into JAX-RS Resource class
#GreetingsResource.java
import javax.ejb.EJB;
#GreetingsResource.java
@Path("greetings")
public class GreetingsResource {
@EJB
private GreetingEJB greetingEJB;
Container locates and injects component that matches declared interface type
Web API needs to report accurate status to caller -- especially when there is an error
EJB interfaces need to make determining that status obvious to detect and easy to report
Business and lower-levels are in best situation to know the specifics of the error
Web API should just be in the role of being the facade that reports it
Use distinct exceptions that map to specific statuses to be returned by the Web API
Built-in JAX-RS Exceptions not valid for use in @Remote interfaces - embedded Response not Serializable
Deliberatly used candidate exception names here -- similar but not identical to JAX-RS exceptions names
Request failed because of a client request error
Informs caller that "it is them and not the service" that has the problem
If they keep making the same request -- they will get the same failure result
public class ClientErrorException extends Exception {
public ClientErrorException(String msg) {
super(msg);
}
}
Extending a checked exception because these errors generally represent business errors
Thrown checked exceptions do not automatically rollback transactions
Client identified resource that cannot be found
public class ResourceNotFoundException extends Exception {
public ResourceNotFoundException(String format, Object...args) {
super(String.format(format, args));
}
public ResourceNotFoundException(String msg) {
super(msg);
}
}
Make it easy for EJB/business logic to report cause ready to return to caller
throw new ResourceNotFoundException("listName[%s] not found", listName);
throw new ResourceNotFoundException("todoList[%s], todoItem[%s] not found", listName, itemName);
Caller targeting a resource that cannot be found
public class InvalidRequestException extends ClientErrorException {
public InvalidRequestException(String format, Object...args) {
super(String.format(format, args));
}
public InvalidRequestException(String msg) {
super(msg);
}
}
throw new InvalidRequestException("Unable to greet, name not supplied");
Request failed because of internal or back-end problem
public class InternalErrorException extends RuntimeException {
public InternalErrorException(String format, Object...args) {
super(String.format(format, args));
}
public InternalErrorException(String msg) {
super(msg);
}
public InternalErrorException(Throwable ex, String msg) {
super(msg, ex);
}
public InternalErrorException(Throwable ex, String format, Object...args) {
super(String.format(format, args), ex);
}
}
Extends un-checked exception since these errors are generally infrastructure-related
Unchecked exceptions automatically rollback current transaction
Exception of value to identify the source of error within the server code
throw new InternalErrorException("Internal error greeting name[%s]: %s", name, ex);
Define basic EJB method
# GreetingEJB.java
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
# GreetingEJB.java
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public String greet(String name) {
return String.format("hello %s", name); //core business code
}
Implements greeting
No transactional behavior -- default for EJB is REQUIRED
Define Resource method in terms of calling EJB method
# GreetingsResource.java
@GET
@Path("greet")
@Produces(MediaType.TEXT_PLAIN)
public Response greet(@QueryParam("name") String name) {
ResponseBuilder rb=null;
String entity = greetingEJB.greet(name);
rb = Response.ok(entity);
return rb.build();
}
Method is accessible at URI /api/greetings/greet
# client
URI uri = UriBuilder.fromUri(baseTodosUrl)
.path("greetings")
.path("greet")
.build();
WebTarget target = client.target(uri);
Method is accessible using GET request and Accepts "text/plain"
# client
Response response = target.request(MediaType.TEXT_PLAIN_TYPE)
.get();
logger.info("GET {} => {}/{}", target.getUri(), response.getStatus(), response.getStatusInfo());
Resource method parameter mapped from HTTP query parameter -- not yet passed
Missing name not yet reported in status
# client GET http://localhost:8080/ejavaTodos/api/greetings/greet => 200/OK hello null
The above should have been reported a detected BAD_REQUEST
We need to fix/implement this
Add error logic to EJB/business method
# GreetingEJB.java
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public String greet(String name) throws InvalidRequestException {
try {
if (name==null || name.isEmpty()) {
throw new InvalidRequestException("Unable to greet, name not supplied");
}
return String.format("hello %s", name); //core business code
} catch (RuntimeException ex) {
throw new InternalErrorException("Internal error greeting name[%s]", name);
}
}
Validates name was provided - reports bad request if missing
Implements greeting
Catches any infrastructure exceptions and reports internal server error with text message for caller
Add minimal exception handling in resource method
# GreetingsResource.java
@GET
@Path("greet")
@Produces(MediaType.TEXT_PLAIN)
public Response greet(@QueryParam("name") String name) {
ResponseBuilder rb=null;
try {
String entity = greetingEJB.greet(name);
rb = Response.ok(entity);
} catch (Exception ex) {
rb=Response.serverError()
.entity(String.format("unexpected error greeting name[%s]", name));
}
return rb.build();
}
We are purposely generalizing our exception logic here as part of incremental example
Resource method currently treating all exceptions as Internal Server Error
# client
WebTarget target = client.target(uri);
Response response = target.request(MediaType.TEXT_PLAIN_TYPE)
.get();
logger.info("GET {} => {}/{}", target.getUri(), response.getStatus(), response.getStatusInfo());
GET http://localhost:8080/ejavaTodos/api/greetings/greet => 500/Internal Server Error unexpected error greeting name[null]
This needs improvement/correction to report correct error type
Add complete error reporting logic to resource method
# GreetingsResource.java
@GET
@Path("greet")
@Produces(MediaType.TEXT_PLAIN)
public Response greet(@QueryParam("name") String name) {
ResponseBuilder rb=null;
try {
String entity = greetingEJB.greet(name);
rb = Response.ok(entity);
} catch (InvalidRequestException ex) {
rb = Response.status(Status.BAD_REQUEST)
.entity(ex.getMessage());
} catch (InternalErrorException ex) {
rb = Response.status(Status.INTERNAL_SERVER_ERROR)
.entity(ex.getMessage());
} catch (Exception ex) {
rb=Response.serverError()
.entity(String.format("unexpected error greeting name[%s]", name));
}
return rb.build();
}
BAD_REQUEST returned for InvalidRequestException -- passing informative message to caller
GET http://localhost:8080/ejavaTodos/api/greetings/greet => 400/Bad Request 19:37:49,825 INFO (GreetingsIT.java:111) -Unable to greet, name not supplied
Client can correct by adding name query parameter
WebTarget target = client.target(uri)
.queryParam("name", "ejava");
Response response = target.request(MediaType.TEXT_PLAIN_TYPE)
.get();
logger.info("GET {} => {}", target.getUri(), response.getStatusInfo());
String greeting = response.readEntity(String.class);
logger.info("{}", greeting);
GET http://localhost:8080/ejavaTodos/api/greetings/greet?name=ejava => OK hello ejava
Web content is shared using many standardized MIME Types
We will address only 2 of them -- both now required by JAX-RS
XML - JAXB support was part of original JAX-RS spec
JSON - requirement for JSONB support was added to JAX-RS in 2.1 as a part of JavaEE 8
No other MIME Types are required by JAX-RS
Any can be added thru standard JAX-RS marshaling/demarshaling framework
Many are immediately available thru vendor extensions
We will show manual approaches to marshaling/demarshaling first
However, content is automatically marshaled/demarshaled by JAX-RS provider
Manual marshaling/demarshaling approaches mainly useful within debug
Sample DTO Class
public class MessageDTO implements Serializable {
private String text;
public MessageDTO() {}
public MessageDTO(String message) {
this.text = message;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
@Override
public String toString() {
return text;
}
}
Implements Serializable to be legal for @Remote interfaces
Typical JavaBean
Default CTOR
public setters/getters
JSON is content type most preferred by Javascript UIs
{"text":"sample text"}
Default marshaling/demarshaling is supported without class modifications
We will look at two JSON marshaling/demarshaling frameworks; JSONB and Jackson
Standard added to JavaEE 8
Start by creating an instance of Jsonb
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.JsonbConfig;
JsonbConfig config=new JsonbConfig();
//config.setProperty(JsonbConfig.FORMATTING, true);
//config.setProperty(JsonbConfig.PROPERTY_NAMING_STRATEGY, PropertyNamingStrategy.LOWER_CASE_WITH_UNDERSCORES);
//config.setProperty(JsonbConfig.NULL_VALUES, true); //helps us spot fields we don't want
Jsonb builder = JsonbBuilder.create(config);
Use Jsonb instance to marshal DTO instance to JSON
protected <T> String marshal(T object) {
if (object==null) { return ""; }
String buffer = builder.toJson(object);
return buffer;
}
Use Jsonb to demarshal DTO instance from JSON
protected <T> T demarshal(Class<T> type, String buffer) {
T result = (T) builder.fromJson(buffer, type);
return result;
}
Direct dependency on API modules useful for pure DTO libraries
Jsonb API
<properties>
<javax.json.bind-api.version>1.0</javax.json.bind-api.version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>javax.json.bind</groupId>
<artifactId>javax.json.bind-api</artifactId>
<version>${javax.json.bind-api.version}</version>
</dependency>
<dependencies>
<!-- defines JSON-B entry point e.g., Jsonb and @Annotations -->
<dependency>
<groupId>javax.json.bind</groupId>
<artifactId>javax.json.bind-api</artifactId>
<scope>provided</scope>
</dependency>
Jsonb Provider
Direct dependency on provider modules useful for pure DTO modules with marshaling tests. JAX-RS provider will automatically bring in these dependencies if part of IT tests.
<properties>
<glassfish-json.version>1.1.2</glassfish-json.version>
<yasson.version>1.0.1</yasson.version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.eclipse</groupId>
<artifactId>yasson</artifactId>
<version>${yasson.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>${glassfish-json.version}</version>
</dependency>
<dependencies>
<!-- JSON-B - reference implementation for JSON-B (e.g., JsonBindingProvider) -->
<dependency>
<groupId>org.eclipse</groupId>
<artifactId>yasson</artifactId>
<scope>test</scope>
</dependency>
<!-- JSON provider for JSON-B reference implementation - needed by yasson -->
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<scope>test</scope>
</dependency>
JBoss/Wildfly default JSON marshaler prior to JavaEE 8
Some inconsistencies with JSON-B relative to collections
Safest to use same marshaler as server
Start by creating an instance of ObjectMapper
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper mapper = new ObjectMapper();
Use ObjectMapper instance to marshal DTO instance to JSON
protected <T> String marshal(T object)
throws JsonGenerationException, JsonMappingException, IOException {
StringWriter buffer = new StringWriter();
mapper.writeValue(buffer, object);
return buffer.toString();
}
Use ObjectMapper to demarshal DTO instance from JSON
protected <T> T demarshal(Class<T> type, String buffer)
throws JsonParseException, JsonMappingException, IOException {
T result = mapper.readValue(buffer, type);
return result;
}
Direct dependency on API modules useful for pure DTO libraries
Jackson API
<properties>
<jackson.version>2.9.5</jackson.version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<scope>provided</scope>
</dependency>
Jackson Provider
Direct dependency on provider modules useful for pure DTO modules with marshaling tests. JAX-RS provider will automatically bring in these dependencies if part of IT tests.
<properties>
<jackson.version>2.9.5</jackson.version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependencies>
<!-- core Jackson2 libraries -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>test</scope>
</dependency>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<message>
<text>sample text</text>
</message>
JAX-RS JAX-B support has existed since the start of JAX-RS
XML is much more precise than JSON -- but with precision and tuning comes complication
Root objects in stream must be annotated as @XmlRootElement
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement(name="message")
public class MessageDTO implements Serializable {
Element namespaces are very common is XML
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:message xmlns:ns2="urn:ejava.jaxrs.todos">
<text>sample text</text>
</ns2:message>
@XmlRootElement(name="message", namespace="urn:ejava.jaxrs.todos")
public class MessageDTO implements Serializable {
Start by creating an instance of JAXBContext appropriate for the specific type
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
JAXBContext jbx = JAXBContext.newInstance(object.getClass());
JAXBContext jbx = JAXBContext.newInstance(type);
Use JAXBContext instance obtain a Marshaller to marshal DTO instance to XML
import javax.xml.bind.Marshaller;
protected <T> String marshal(T object) throws JAXBException {
if (object==null) { return ""; }
JAXBContext jbx = JAXBContext.newInstance(object.getClass());
Marshaller marshaller = jbx.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
StringWriter buffer = new StringWriter();
marshaller.marshal(object, buffer);
return buffer.toString();
}
Use JAXBContext instance to obtain an Unmarshaller to demarshal DTO instance from XML
import javax.xml.bind.Unmarshaller;
protected <T> T demarshal(Class<T> type, String buffer) throws JAXBException {
if (buffer==null) { return null; }
JAXBContext jbx = JAXBContext.newInstance(type);
Unmarshaller unmarshaller = jbx.createUnmarshaller();
ByteArrayInputStream bis = new ByteArrayInputStream(buffer.getBytes());
@SuppressWarnings("unchecked")
T result = (T) unmarshaller.unmarshal(bis);
return result;
}
Ignore property
@XmlTransient
Represent as XML attribute (versus a child element)
@XmlAttribute
JAX-RS defines an encoding/decoding framework to marshal/demarshal content
Provider will automatically perform this work for JSON, XML, and vendor-specific supported content types
Pass result of Entity.entity() with target mediaType and annotations to guide marshaling choices
# client
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
# client
Response response = target.request(mediaType)
.buildPost(Entity.entity(todoList, mediaType, todoList.getClass().getAnnotations()))
.invoke();
Provider selects matching URI path method that can accept content type and passes as method parameter
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@POST
@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public Response createTodoList(TodoListDTO todoList) {
Set Response.entity to response payload
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@POST
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public Response createTodoList(TodoListDTO todoList) {
TodoListDTO entity = todosMgmt.createTodoList(todoList);
URI location = ...
ResponseBuilder rb = Response.created(location)
.contentLocation(location)
.entity(entity);
return rb.build();
Pass Response.readEntity the type of object expected and annotations to guide demarshaling choices
# client
import javax.ws.rs.core.Response;
# client
static <T> T getEntity(Response response, Class<T> type) {
if (Response.Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
return response.readEntity(type, type.getAnnotations());
} else {
throw new IllegalStateException(String.format("error response[%d %s]: %s",
response.getStatus(),
response.getStatusInfo(),
response.readEntity(String.class))
);
}
}
Content providers only necessary for IT tests. Application server will have all API and implementation modules server-side.
Content Provider Maven modules
<properties>
<resteasy.version>3.5.1.Final</resteasy.version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-json-binding-provider</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxb-provider</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependencies>
<!-- JSON-B JSON wiring for RESTEasy JAX-RS provider (javaee8; highest JSON priority) -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-json-binding-provider</artifactId>
<scope>test</scope>
</dependency>
<!-- JSON wiring for RESTEasy JAX-RS provider (javaee7) -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<scope>test</scope>
</dependency>
<!-- XML impl for RESTEasy JAX-RS provider -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxb-provider</artifactId>
<scope>test</scope>
</dependency>
If you taking a minimalist approach of least configuration and using JavaEE 7 Wildfly -- you will want to use Jackson for JSON marshaling. If you are using JavaEE 8, then you will want to use Jsonb. The two can be used together -- but you must be more aware of the tweaks required to have the provider chose one over the other.
Annotated with @Path
Injected with implementation details and call context
TodoList Example
import javax.ejb.EJB;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriInfo;
import info.ejava.examples.jaxrs.todos.ejb.TodosMgmtRemote;
import javax.ejb.EJB;
@Path("todo_lists")
public class TodoListsResource {
@EJB
private TodosMgmtRemote todosMgmt;
@Context
private UriInfo uriInfo;
...
}
Products Example
@Path("products")
public class ProductsResource {
@EJB
private InventoryMgmtEJB ejb;
@Context
private Request request;
@Context
private UriInfo uriInfo;
...
}
Get members of a resource
Could add query parameters
Common to add paging
@GET @Path("")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public Response getTodoLists(
@QueryParam("offset") @DefaultValue("0") Integer offset,
@QueryParam("limit") @DefaultValue("10") Integer limit) {
ResponseBuilder rb = null;
try {
TodoListListDTO entity = todosMgmt.getTodoLists(offset, limit);
rb = Response.ok(entity)
.contentLocation(uriInfo.getAbsolutePath());
} catch (InternalErrorException ex) {
rb = Response.serverError().entity(ex.getMessage());
} catch (Exception ex) {
logger.info("Unexpected exception getting TodoLists", ex);
String msg = String.format("Unexpected error getting TodoLists: %s", ex.toString());
rb = Response.serverError()
.entity(new MessageDTO(msg));
}
return rb.build();
}
@Path("") - method applied to URI path of overall JAX-RS class
@Produces - indicates Content-Type results are available for
@QueryParam - maps query parameter to input method argument
@DefaultValue - assigns a value to be applied when query parameter not supplied by caller
contentLocation() - returns Content-Location header indicating URI for payload
uriInfo - injected resource that knows the calling URI context for method
# client
private UriBuilder getBaseUrl(String...path) {
UriBuilder builder = UriBuilder.fromUri(baseUrl);
if (path!=null) {
for (String p:path) {
builder = builder.path(p);
}
}
return builder;
}
UriBuilder - used to form a URL to resource
# client
static String TODO_LISTS_PATH = "todo_lists";
# client
public Response getTodoLists(Integer offset, Integer limit) {
URI uri = getBaseUrl(TODO_LISTS_PATH).build();
WebTarget target = client.target(uri);
if (offset!=null) {
target=target.queryParam(OFFSET, offset);
}
if (limit!=null) {
target=target.queryParam(LIMIT, limit);
}
return target.request(mediaType)
.buildGet()
.invoke();
}
queryParam - used to pass named query parameters to method
mediaType - used to indicate which media types willing to accept
Response - directly returned for inspection
# client
static <T> T getEntity(Response response, Class<T> type) {
if (Response.Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
return response.readEntity(type, type.getAnnotations());
} else {
Helper method to inspect Response and return instance if success
Inspection may need to look at headers
Content-Type dictated by API return payload
# client
Response response = todosClient.getTodoLists(null, null);
TodoListListDTO todoLists = getEntity(response, TodoListListDTO.class);
Caller indicates Java type for payload returned
Create new resource or tunnel service
Returns CREATED and URI of created resource
private ResponseBuilder getBadRequestResponse(Exception ex) {
logger.debug(ex.getMessage());
return Response.status(Status.BAD_REQUEST)
.entity(new MessageDTO(ex.getMessage()));
}
Simple helper method to build status-specific response
MessageDTO used to report error text -- still targeted mediaType
@POST
@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public Response createTodoList(TodoListDTO todoList) {
ResponseBuilder rb = null;
try {
TodoListDTO entity = todosMgmt.createTodoList(todoList);
URI location = uriInfo.getBaseUriBuilder()
.path(TodoListsResource.class)
.path(TodoListsResource.class, "getTodoList")
.build(entity.getName());
rb = Response.created(location)
.contentLocation(location)
.entity(entity);
} catch (InvalidRequestException ex) {
rb = getBadRequestResponse(ex);
} catch (InternalErrorException ex) {
rb = getInternalErrorResponse(ex);
} catch (Exception ex) {
rb = getUndexpectedErrorResponse("Unexpected error creating TodoList", ex);
}
return rb.build();
}
@POST - makes the method available for the resource
created() - helper method to indicate a 201/CREATED status and a Location header
uriInfo.getBaseUriBuilder() - access to a URIBuilder to build locationj URI
# client
public Response createTodoList(TodoListDTO todoList) {
URI uri = getBaseUrl(TODO_LISTS_PATH).build();
WebTarget target = client.target(uri);
return target.request(mediaType)
.buildPost(Entity.entity(todoList, mediaType, todoList.getClass().getAnnotations()))
.invoke();
}
Entity.entity() - used to define request content
buildPost() - accepts entity definition or null if no payload content
# client
Response response = todosClient.createTodoList(todoList);
TodoListDTO createdList = getEntity(response,TodoListDTO.class);
Non-destructive read
Returns 200/OK with payload
@GET @Path("{listName}")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public Response getTodoList(@PathParam("listName") String listName) {
ResponseBuilder rb = null;
try {
TodoListDTO entity = todosMgmt.getTodoList(listName);
rb = Response.ok(entity)
.contentLocation(uriInfo.getAbsolutePath());
} catch (ResourceNotFoundException ex) {
rb = getNotFoundResponse(ex);
} catch (InternalErrorException ex) {
rb = getInternalErrorResponse(ex);
} catch (Exception ex) {
rb = getUndexpectedErrorResponse("Unexpected error creating TodoList", ex);
}
return rb.build();
}
@Path defines required parameter(s) using {variable} syntax
@PathParam maps "listName" from URI path to String listName argument to method
@DefaultValue("value") can be applied -- otherwise null if not supplied
Since this is the GET for a single resource -- the contentLocation will be the exact URI that called this endpoint
# client
static String TODO_LIST_PATH = "todo_lists/{listName}";
String URI is defined with {variable} syntax
{variable} - in this case - identifies which todo_list resource within the collection
# client
public Response getTodoList(String listName) {
URI uri = getBaseUrl(TODO_LIST_PATH).build(listName);
WebTarget target = client.target(uri);
return target.request(mediaType)
.buildGet()
.invoke();
}
build() - accepts {variable} values in sequence order
# client
//request a resource that does exist
Response response = todosClient.getTodoList(todoList.getName());
TodoListDTO resultList = getEntity(response, TodoListDTO.class);
Specific Resource will be marshaled back to client using DTO view
# client
//request a resource that does not exist
Response response = todosClient.getTodoList("foobar_not_exist");
assertEquals("unexpected error family",
Status.Family.CLIENT_ERROR,
response.getStatusInfo().getFamily());
assertEquals("unexpected status",
Status.NOT_FOUND,
response.getStatusInfo());
NOT_FOUND CLIENT_ERROR will be returned to client for unknown resource ID
Update existing or create well-known URI
Optional results
In this case we will work with nested resource
@PUT @Path("{listName}/todo_items/{itemName}")
@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public Response updateTodoItem(
@PathParam("listName") String listName,
@PathParam("itemName") String itemName,
TodoItemDTO item) {
ResponseBuilder rb = null;
try {
TodoItemDTO entity = todosMgmt.updateTodoListItem(listName, itemName, item);
rb = Response.ok(entity);
} catch (ResourceNotFoundException ex) {
rb = getNotFoundResponse(ex);
} catch (InvalidRequestException ex) {
rb = getBadRequestResponse(ex);
} catch (InternalErrorException ex) {
rb = getInternalErrorResponse(ex);
} catch (Exception ex) {
rb = getUndexpectedErrorResponse("Unexpected error updating TodoItems", ex);
}
return rb.build();
}
Our todoItem is below a specific todoList
Two {variables} exist in path to represent the parent and child IDs
We are using a PUT since we know the exact URI for the resource
# client
static String TODO_LIST_PATH = "todo_lists/{listName}";
static String TODO_ITEM_PATH = "todo_items/{itemName}";
# client
public Response updateTodoItem(String listName, TodoItemDTO item) {
URI uri = getBaseUrl(TODO_LIST_PATH, TODO_ITEM_PATH).build(listName, item.getName());
WebTarget target = client.target(uri);
return target.request(mediaType)
.buildPut(Entity.entity(item, mediaType, item.getClass().getAnnotations()))
.invoke();
}
Our URI has two {variables} and build() requires two sequential values
buildPut() - accepts payload definition same as earlier buildPost()
# client
Response response = todosClient.updateTodoItem(todoList.getName(), item);
TodoItemDTO updated = getEntity(response, item),TodoItemDTO.class);
Caller must supply context information to fill in nested resource path
Deletes specified resource
Optional results
@DELETE @Path("{listName}")
public Response deleteTodoList(@PathParam("listName") String listName) {
ResponseBuilder rb = null;
try {
todosMgmt.deleteTodoList(listName);
rb = Response.noContent();
} catch (ResourceNotFoundException ex) {
rb = getNotFoundResponse(ex);
} catch (InternalErrorException ex) {
rb = getInternalErrorResponse(ex);
} catch (Exception ex) {
rb = getUndexpectedErrorResponse("Unexpected error deleting TodoList", ex);
}
return rb.build();
}
DELETE method signals action to take with identified resource
noContent() - a variant of 200/OK with Content-Length=0; statusCode=204
# client
static String TODO_LIST_PATH = "todo_lists/{listName}";
# client
public Response deleteTodoList(String listName) {
URI uri = getBaseUrl(TODO_LIST_PATH).build(listName);
WebTarget target = client.target(uri);
return target.request(mediaType)
.buildDelete()
.invoke();
}
buildDelete() - builds request similar to others except with DELETE as the verb
# client
Response response = todosClient.deleteTodoList(todoList.getName());
assertSuccess("error deleting todoList", response);
Caller inspects Response for successful status code
# client
static <T> void assertSuccess(String message, Response response) {
if (!Response.Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily())) {
throw new IllegalStateException(String.format(message + ", error response[%d %s]: %s",
response.getStatus(),
response.getStatusInfo(),
response.readEntity(String.class))
);
} else {
response.close();
}
}
SUCCESSFUL family check avoids 200/OK versus 204/NO_CONTENT confusion
Table of Contents
At the completion of this topic, the student shall
have more understanding of:
the basic concepts behind JavaEE Access Control
what a Security Realm is
authentication thru JNDI
how to runAs with alternate roles and alternate user identity within JavaEE
how to secure and authorize Web API access
be able to:
define access control policy for EJB and Web API applications
authenticate and obtain proper access to the application
implement RMI authentication using JNDI InitialContext
implement HTTP Basic authentication using JAX-RS
EJB access restrictions
Declarative
Programmatic
EJB assignment to Security Domain
Server definition of Security Domain
Server Security Domain authentication and authorization
Figure 112.1. Declarative EJB Access Control: Annotations
@Stateless(name="SecurePingEJB")
public class SecurePingEJB implements SecurePingRemote, SecurePingLocal {
...
@PermitAll
public String pingAll() {
return getInfo("pingAll");
}
@RolesAllowed({"user"})
public String pingUser() {
return getInfo("pingUser");
}
@RolesAllowed({"admin"})
public String pingAdmin() {
return getInfo("pingAdmin");
}
@DenyAll
public String pingExcluded() {
return getInfo("pingExcluded");
}
}
javax.annotation.security
annotations define caller role requirements to access EJB methods
role can be literal or a logical mapping (see Role Mapping below)
Figure 112.2. Declarative EJB Access Control: ejb-jar.xml
<assembly-descriptor>
...
<method-permission>
<unchecked/>
<method>
<ejb-name>SecurePingEJB</ejb-name>
<method-name>pingAllmethod-name>
</method>
</method-permission>
<method-permission>
<role-name>admin</role-name>
<method>
<ejb-name>SecurePingEJB</ejb-name>
<method-name>pingAdmin</method-name>
</method>
</method-permission>
<method-permission>
<role-name>user</role-name>
<method>
<ejb-name>SecurePingEJB</ejb-name>
<method-name>pingUser</method-name>
</method>
</method-permission>
<method-permission>
<excluded/>
<method>
<ejb-name>SecurePingEJB</ejb-name>
<method-name>pingExcluded</method-name>
</method>
</method-permission>
</assembly-descriptor>
Access restrictions can also be defined in the ejb-jar.xml deployment descriptor
Figure 112.3. Programmatic Role Check
@Resource
SessionContext ctx;
if (!ctx.isCallerInRole(role)) {
throw new EJBAccessException("...");
}
Permits method to determine when access should be checked
Figure 112.4. Programmatic Property Check
@Resource
SessionContext ctx;
@RolesAllowed({"admin"})
public String updateGreeting(String greeting) {
String login = ctx.getCallerPrincipal().getName();
Office office = getOfficeByAdmin(login);
if (office!=null) {
office.setGreeting(greeting);
}
Permits more fine-grain access control down to the object level
Caller is not only required to have "admin" role -- they must be admin of specific resource
Permits role-name within Java code to be mapped to security role
No annotation for this. Must use descriptor or update referenced roles
<enterprise-beans>
<session>
<ejb-name>SecurePingEJB</ejb-name>
<security-role-ref>
<description>role-name checked within EJB</description>
<role-name>internalRole</role-name>
<role-link>admin</role-link>
</security-role-ref>
</session>
</enterprise-beans>
<assembly-descriptor>
<security-role>
<role-name>admin</role-name>
</security-role>
<security-role>
<role-name>user</role-name>
</security-role>
</assembly-descriptor>
<?xml version="1.0"?>
<jboss:ejb-jar
xmlns:jboss="http://www.jboss.com/xml/ns/javaee"
xmlns:sec="urn:security"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-ejb3-2_0.xsd
http://java.sun.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-ejb3-spec-2_0.xsd
urn:security urn:security"
version="3.1"
impl-version="2.0">
<assembly-descriptor>
<sec:security>
<ejb-name>*</ejb-name>
<sec:security-domain>other</sec:security-domain>
<sec:run-as-principal/>
</sec:security>
</assembly-descriptor>
</jboss:ejb-jar>
security element assigns security properties to one or more EJBs
security-domain assigns to a specific security-domain defined on server
"other" is default security-domain
run-as-principal defines the identity to authenticate and execute the EJB calls with
run-as-principal is default - call is authorized based on the caller's identity
Legacy implementation (covered below) [15]
<server xmlns="urn:jboss:domain:7.0">
<management>
<security-realms>
...
</security-realms>
...
<subsystem xmlns="urn:jboss:domain:security:2.0">
<security-domains>
...
</security-domains>
</subsystem>
Latest implementation: "Elytron" (not covered) [16]
<subsystem xmlns="urn:wildfly:elytron:3.0" final-providers="combined-providers"
disallowed-providers="OracleUcrypto">
<security-domains>
...
</security-domains>
<security-realms>
...
</security-realms>
</subsystem>
Integrates identity store into application server
Verifies credentials
Obtains attributes associated with identity
Performs role mappings
ManagementRealm and ApplicationRealm supplied out of the box
# standalone.xml
<server xmlns="urn:jboss:domain:7.0">
<management>
<security-realms>
<security-realm name="ApplicationRealm">
<server-identities>
<ssl>
<keystore path="application.keystore"
relative-to="jboss.server.config.dir"
keystore-password="password"
alias="server"
key-password="password"
generate-self-signed-certificate-host="localhost"/>
</ssl>
</server-identities>
<authentication>
<local default-user="$local" allowed-users="*" skip-group-loading="true"/>
<properties path="application-users.properties"
relative-to="jboss.server.config.dir"/>
</authentication>
<authorization>
<properties path="application-roles.properties"
relative-to="jboss.server.config.dir"/>
</authorization>
</security-realm>
</security-realms>
server-identities
Supplies a server identity to clients for inbound connections
Supplies a client identity for outbound connections (e.g., username/password)
In this example, server identity is supplied from an SSL keystore
authentication
Identifies which credential stores are used to authenticate incoming connections
In this example, user credentials are stored in a static file with username and password
"local"
Allows local users to connect to server without a password
Leverages a token written to the filesystem and judges read/write permissions of the client
Anonymous local client will have identity of "$local"
Local client may specify any username in "allowed-users" without a password
Applies to only JBoss Remoting/RMI callers
authorization
In this example, user roles are stored in a static file with username and roles
JBoss can operate in a mode to trust an RMI user connecting from the same machine and running under the same operating system identity that launched the server. This allows for development scenarios to bypass login credentials and Command Line Interface (CLI) to operate without credential prompts.
<local default-user="$local" allowed-users="*" skip-group-loading="true"/>
<subsystem xmlns="urn:jboss:domain:remoting:4.0">
<http-connector name="http-remoting-connector"
connector-ref="default"
security-realm="ApplicationRealm"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:undertow:6.0" default-server="default-server"
default-virtual-host="default-host"
default-servlet-container="default"
default-security-domain="other"
statistics-enabled="true">
<server name="default-server">
...
<https-listener name="https" socket-binding="https"
security-realm="ApplicationRealm"
enable-http2="true"/>
<host name="default-host" alias="localhost">
<location name="/" handler="welcome-content"/>
<http-invoker security-realm="ApplicationRealm"/>
</host>
High-level security policies backed by one or more Security Realms
Can be shared across multiple applications
Results in a Security Identity for the identity of the current caller
Default Security Domain for applications
Two login modules
Remoting leverages information from Remoting connection if used
RealmDirect delegates to SecurityRealm (default=ApplicationRealm)
<subsystem xmlns="urn:jboss:domain:security:2.0">
<security-domains>
<security-domain name="other" cache-type="default">
<authentication>
<login-module code="Remoting" flag="optional">
<module-option name="password-stacking" value="useFirstPass"/>
</login-module>
<login-module code="RealmDirect" flag="required">
<module-option name="password-stacking" value="useFirstPass"/>
</login-module>
</authentication>
</security-domain>
<subsystem xmlns="urn:jboss:domain:ejb3:5.0">
...
<default-security-domain value="other"/>
...
</subsystem>
<subsystem xmlns="urn:jboss:domain:undertow:6.0" default-server="default-server"
default-virtual-host="default-host"
default-servlet-container="default"
default-security-domain="other"
statistics-enabled="true">
...
</subsystem>
JBoss installs with a set of static files to implement user authentication and authorization. This can be augmented or replaced by more dynamic sources such as a database or LDAP.
Figure 113.1. Example Authentication Source: application-users.properties
$ cat standalone/configuration/application-users.properties
known=0aea67d2fdd25e4207f0bc2a5c84a1a6
user1=b802b482f67706cb453ab9294b88a9ea
admin1=7357a05355829fb2855412b69297a55b
publisher1=a15cfe086e59fea3fe4c787b834f55b8
subscriber1=838a4d7dceb6655e0297947d9677c6cb
requestor1=21a138aa0eeec44ee9cf08ebd9a1dcb5
worker1=41c30fe6af7d12f7b7fd9aa60060f13d
admin2=f064c7710b767a156f7e07469f8ad944
Authenticates identity of user
Username and hashed password stored in static file
Figure 113.2. Example Access Control Source: application-roles.properties
$ cat standalone/configuration/application-roles.properties
user1=user,esales-user
admin1=user,admin,dmv-admin,mayberry-admin,eleague-admin,eclub-admin
publisher1=publisher
subscriber1=subscriber
requestor1=requestor
worker1=worker
admin2=admin
Assigns roles to authenticated user
Username and roles stored in static file
Legacy and Elytron serve the same authentication and authorization purposes
SecurityRealm addresses details of authentication and obtaining user properties
SecurityDomain contains higher level instructions - including the map realm properties into roles
JBoss Remoting and JNDI InitialContext
EJBClient and JNDI InitialContext
Changing Users
Access Violations
Access Granted
Used to perform JMS and other non-EJB resource lookups
java.naming.factory.initial=org.wildfly.naming.client.WildFlyInitialContextFactory java.naming.factory.url.pkgs= java.naming.provider.url=http-remoting://127.0.0.1:8080
factory.initial set to WildFlyInitialContextFactory implementation
compatible with both JBoss Remoting and EJBClient
provider.url set to address of JBoss server
url.pkgs option not needed with Wildfly implementation (e.g., "ejb:")
securePingEAR/securePingEJB/SecurePingEJB!info.ejava.examples.secureping.ejb.SecurePingRemote
Using generic JBoss Remoting JNDI name
private Context runAs(String[] login) throws NamingException, IOException {
Properties env = new Properties(); //load the defaults from jndi.properties
if (login != null) {
env.put(Context.SECURITY_PRINCIPAL, login[0]);
env.put(Context.SECURITY_CREDENTIALS, login[1]);
}
return new InitialContext(env);
}
Client provides credentials in JNDI prior to obtaining InitialContext
Must use current JNDI Context to lookup @Remote
Must leave JNDI Context open while making calls on @Remote interface
protected String[] userLogin = new String[] { userUser, userPassword };
protected String[] adminLogin = new String[] { adminUser, adminPassword };
jndi=runAs(userLogin);
ejb=(SecurePing)jndi.lookup(jndiName);
assertFalse("user in admin role", ejb.isCallerInRole("admin"));
assertTrue("user not in user role", ejb.isCallerInRole("user"));
jndi.close();
jndi=runAs(adminLogin);
ejb=(SecurePing)jndi.lookup(jndiName);
assertTrue("admin not in admin role", ejb.isCallerInRole("admin"));
assertTrue("admin not in user role", ejb.isCallerInRole("user"));
jndi.close();
Client can switch credentials with a change in InitialContexts and @Remote reference
Primary means to communicate with EJB using RMI
Leverages vendor knowledge of EJB and specific @Remote interface
java.naming.factory.initial=org.wildfly.naming.client.WildFlyInitialContextFactory java.naming.factory.url.pkgs= java.naming.provider.url=http-remoting://127.0.0.1:8080
factory.initial not important if url.pkgs extension supplied
WildFlyInitialContextFactory performs this automatically
provider.url same as with JBoss Remoting
ejb:securePingEAR/securePingEJB/SecurePingEJB!info.ejava.examples.secureping.ejb.SecurePingRemote
Using EJBClient JNDI name prefix ("ejb:")
protected static String[] currentLogin;
private void runAs(String[] login) throws NamingException, IOException {
if (!Arrays.equals(login, currentLogin) || securePing==null) {
Properties props = new Properties(); //initialize with values from jndi.properties
if (login!=null) {
props.put(Context.SECURITY_PRINCIPAL, login[0]);
props.put(Context.SECURITY_CREDENTIALS, login[1]);
}
InitialContext jndi = null;
try {
jndi = new InitialContext(props);
securePing = (SecurePingRemote)jndi.lookup(jndiName);
currentLogin = login;
} finally {
if (jndi!=null) { jndi.close(); }
}
}
}
EJBClient stores credentials with connection
No security need to keep InitialContext open
protected String[] userLogin = new String[] { userUser, userPassword };
protected String[] adminLogin = new String[] { adminUser, adminPassword };
runAs(userLogin);
assertFalse("user in admin role", securePing.isCallerInRole("admin"));
assertTrue("user not in user role", securePing.isCallerInRole("user"));
runAs(adminLogin);
assertTrue("admin not in admin role", securePing.isCallerInRole("admin"));
assertTrue("admin not in user role", securePing.isCallerInRole("user"));
No need to get new InitialContext or new lookup of @Remote
Useful to implement sanity check to assure authentication and authorizations in place
@Stateless
public class SecurePingEJB implements SecurePingRemote, SecurePingLocal {
@Resource
SessionContext ctx;
@PermitAll
public String whoAmI() {
String name= ctx.getCallerPrincipal().getName();
logger.debug("whoAmI()={}", name);
return name;
}
Implement debug method to return authenticated identity
@PermitAll
public boolean isCallerInRole(String role) {
boolean result = ctx.isCallerInRole(role);
logger.debug("user={}, isCallerInRole({})={}", ctx.getCallerPrincipal().getName(), role, result);
return result;
}
Implement debug method to return role query result
runAs(userLogin);
assertEquals("unexpected user", userUser, securePing.whoAmI());
assertFalse("user in internalRole role", securePing.isCallerInRole("internalRole"));
runAs(adminLogin);
assertEquals("unexpected user", adminUser, securePing.whoAmI());
assertTrue("admin not in internalRole role", securePing.isCallerInRole("internalRole"));
Client asserts security query results to verify setup correctly
Context jndi = new InitialContext();
logger.debug("looking up jndi.name={}", jndiName);
securePing = (SecurePingRemote)jndi.lookup(jndiName);
try {
runAs(userLogin);
logger.info(securePing.pingAdmin());
fail("didn't detect non-admin user");
}
catch (EJBAccessException ex) {
logger.info("expected exception thrown:" + ex);
}
-looking up jndi.name=ejb:securePingEAR/securePingEJB/SecurePingEJB !info.ejava.examples.secureping.ejb.SecurePingRemote as user1 -found=Proxy for remote EJB StatelessEJBLocator for "securePingEAR/securePingEJB/SecurePingEJB", view is interface info.ejava.examples.secureping.ejb.SecurePingRemote, affinity is None -login=[user1, password1!], whoAmI=user1 -expected exception thrown:javax.ejb.EJBAccessException: WFLYEJB0364: Invocation on method: public abstract java.lang.String info.ejava.examples.secureping.ejb.SecurePing.pingAdmin() of bean: SecurePingEJB is not allowed
EJBAccessException thrown when accessing method not allowed
runAs(adminLogin);
logger.info(securePing.pingAdmin());
-looking up jndi.name=ejb:securePingEAR/securePingEJB/SecurePingEJB !info.ejava.examples.secureping.ejb.SecurePingRemote as admin1 -found=Proxy for remote EJB StatelessEJBLocator for "securePingEAR/securePingEJB/SecurePingEJB", view is interface info.ejava.examples.secureping.ejb.SecurePingRemote, affinity is None -login=[admin1, password1!], whoAmI=admin1 -called pingAdmin, principal=admin1, isUser=true, isAdmin=true, isInternalRole=true
Access to method protected by declarative security granted
Results of programmatic security checks returned in formatted text string
No caller context
Elevate access
Run-as role-name and identity
security-identity of bean defaults to caller identity
import javax.annotation.security.RunAs;
@Stateless
@PermitAll
@RunAs("admin")
public class SecurePingClientEJB
implements SecurePingClientRemote, SecurePingClientLocal {
@EJB(lookup="ejb:securePingEAR/securePingEJB/SecurePingEJB!info.ejava.examples.secureping.ejb.SecurePingRemote")
SecurePingRemote securePingServer;
EJB can discard caller's identity/roles and run-as a specific role
Provide the roleName (e.g., "admin") you wish the method to execute with and *not* the userName (e.g., "admin1") when using the @RunAs annotation.
# jboss-web.xml
<assembly-descriptor>
<sec:security>
<ejb-name>*</ejb-name>
<sec:security-domain>other</sec:security-domain>
<sec:run-as-principal>admin1</sec:run-as-principal>
</sec:security>
</assembly-descriptor>
Specifies specific user-identity to run-as
Vendor-specific
Use jboss-ejb3.xml elements to define caller identity if overriding or supplying caller identity. Otherwise caller identity will be blank.
Method invoked as "user1" with role "user"
runAs(userLogin);
logger.info(securePing.pingAdmin());
Method configured to execute with "admin" role
@RunAs("admin")
public String pingAdmin() {
return securePingServer.pingAdmin();
}
Method successfully invokes second EJB method requiring "admin" role
-looking up jndi.name=ejb:securePingClientEAR/securePingClientEJB/SecurePingClientEJB !info.ejava.examples.secureping.ejb.SecurePingClientRemote as user1 -found=Proxy for remote EJB StatelessEJBLocator for "securePingClientEAR/securePingClientEJB/SecurePingClientEJB", view is interface info.ejava.examples.secureping.ejb.SecurePingClientRemote, affinity is None -login=[user1, password1!], whoAmI=user1 -securePingClient called pingAdmin, principal=user1, <== output from EJB isUser=true, invoked by client isAdmin=false, isInternalRole=false: securePing=called pingAdmin, principal=admin1, <== output from EJB isUser=false, invoked by @RunAs EJB isAdmin=true, isInternalRole=true
user1 is allowed to call method restricted to admin when proxied by run-as EJB
proxy EJB sees caller as user1 and having user1 assigned roles
proxied EJB sees caller as admin1 and only having assigned admin role
HTTP BASIC authentification with JAX-RS
HTTPS private connections
JAX-RS Filters
Declarative Access Control
Add/modify deployment descriptors in WEB-INF
$ jar tf target/securePingJaxRsWAR-5.0.0-SNAPSHOT.war ... WEB-INF/beans.xml WEB-INF/web.xml WEB-INF/jboss-web.xml
# jboss-web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss-web>
<jboss-web xmlns="http://www.jboss.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-web_12_0.xsd"
version="12.0">
<security-domain>other</security-domain>
</jboss-web>
default-security domain was assigned within Wildfly configuration - making this optional
BASIC - username and password passed in "Authentication" header Base64 encoded
FORM - credentials submitted as part of a form response
CLIENT-CERT - client public key authenticated as part of HTTPS connection
DIGEST - an encyrpted form of BASIC
EXTERNAL
# web.xml
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="4.0">
...
</web-app>
# web.xml
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>ApplicationRealm</realm-name>
</login-config>
May need separate WARs for mixed solutions using BASIC (API) and FORM
Wildfly legacy security offers the following option
# web.xml
<!-- if mixing JAX-RS BASIC with HTML FORM
http://undertow.io/undertow-docs/undertow-docs-1.3.0/index.html#servlet-security
-->
<auth-method>BASIC?silent=true,FORM</auth-method>
API will silently accept BASIC Authorization header if supplied
API will not provide any response codes or headers making browser believe it accepts BASIC
Web UI will act as if it only uses FORM
@ApplicationPath("api")
public class SecurePingJaxRsApplication extends Application {
@Path("ping")
public class SecurePingResource {
//this injection requires CDI, which requires a WEB-INF/beans.xml file be in place to activate
@EJB(beanName="SecurePingEJB", beanInterface=SecurePingLocal.class)
private SecurePing secureService;
@Context
private SecurityContext ctx;
Accessible via "/api/ping"
Injected with EJB for business tier integration
Injected with SecurityContext for programmatic security checks
Return the authenticated userName of the caller from the EJB tier
@Path("whoAmI")
@GET
@Produces(MediaType.TEXT_PLAIN)
public Response whoAmI() {
ResponseBuilder rb = null;
try {
rb = Response.ok(secureService.whoAmI());
} catch (Exception ex) {
rb=makeExceptionResponse(ex);
}
return rb.build();
}
Accessible via GET "/api/ping/whoAmI"
Return the role query result for current user from EJB tier
@Path("roles/{role}")
@GET
@Produces(MediaType.TEXT_PLAIN)
public Response isCallerInRole(@PathParam("role") String role) {
ResponseBuilder rb = null;
try {
rb = Response.ok(secureService.isCallerInRole(role));
} catch (Exception ex) {
rb=makeExceptionResponse(ex);
}
return rb.build();
}
Accessible via GET "/api/ping/roles/{role}"
Form the Authorization value for the header
# client
String credentials = userUser + ":" + userPassword;
String authn = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes());
Results in the following value
Basic dXNlcjE6cGFzc3dvcmQxIQ==
Add Authorizaton header to JAX-RS request
# client
Response response = target.request(MediaType.TEXT_PLAIN)
.header("Authorization", authn)
.get();
Results in the following header being addedi to request
Authorization: Basic dXNlcjE6cGFzc3dvcmQxIQ==
Above unwrapped solution works - but mixes business communication details with security details
Better to use a filter - promotes separation of concerns
import java.io.IOException;
import java.util.Base64;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.core.MultivaluedMap;
public class BasicAuthnFilter implements ClientRequestFilter {
private final String authn;
public BasicAuthnFilter(String username, String password) {
String credentials = username + ":" + password;
authn = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes());
}
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
headers.add("Authorization", authn);
}
}
Instantiate the filter
# client
ClientRequestFilter authnFilter = new BasicAuthnFilter(userUser, userPassword);
Register filter with JAX-RS Client
# client
Client jaxRsClient = ClientBuilder.newClient();
jaxRsClient.register(authnFilter);
Leave remaining calls untouched
# client
URI whoAmIUri = UriBuilder.fromUri(baseHttpUrl).path("whoAmI").build();
WebTarget target = jaxRsClient.target(whoAmIUri);
logger.debug("GET {}", target.getUri());
Response response = target.request(MediaType.TEXT_PLAIN).get();
If switching identities, you will need multiple JAX-RS Client instances. Best performance to cache Client instances rather than re-construct each and their filter chain each time.
private Map<String, Client> jaxRsClients = new HashMap<>();
Problem - HTTP will send credential in the clear
Base64 is an encoding - not an encyption
NONE - (default) no confidentiality required (HTTP)
CONFIDENTIAL - confidentiality required (HTTPS)
# web.xml
<security-constraint>
<web-resource-collection>
<web-resource-name>methods</web-resource-name>
<url-pattern>/api/ping/whoAmI</url-pattern>
<url-pattern>/api/ping/canAccess</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
Specifying CONFIDENTIAL will disable HTTP for url-pattern and cause re-direct to HTTPS
Inspect response headers using Filter
ClientResponseFilter provides convenient access to request and response for debug
# client
public class LoggingFilter implements ClientResponseFilter {
private final Logger logger;
public LoggingFilter(Logger logger) {
this.logger = logger;
}
@Override
public void filter(ClientRequestContext requestContext,
ClientResponseContext responseContext) {
String method = requestContext.getMethod();
String uri = requestContext.getUri().toString();
StatusType status = responseContext.getStatusInfo();
logger.debug("{} {}, returned {}/{}\nhdrs sent: {}\nhdrs rcvd: {}",
method, uri, status.getStatusCode(), status,
requestContext.getStringHeaders(),
responseContext.getHeaders());
}
}
Register Filter with Client
# client
ClientResponseFilter loggingFilter = new LoggingFilter(logger);
jaxRsClient.register(loggingFilter);
HTTP Headers - Redirect
# client -GET http://localhost:8080/securePingApi/api/ping/whoAmI, returned 302/Found hdrs sent: [Accept=text/plain, Authorization=Basic dXNlcjE6cGFzc3dvcmQxIQ==] hdrs rcvd: [Connection=keep-alive, Content-Length=0, Date=Tue, 13 Nov 2018 14:03:28 GMT, Location=https://localhost:8443/securePingApi/api/ping/whoAmI]
302/Found response re-directs to Location
Location contains full HTTPS URL
Use HTTPS URL
Change URL to use HTTPS versus HTTP
This is same URL returned in 302/FOUND re-direct
# client
if (response.getStatus()==302) {
String redirectTo = response.getHeaderString("Location");
target = jaxRsClient.target(redirectTo);
logger.debug("GET {}", target.getUri());
response = target.request(MediaType.TEXT_PLAIN).get();
}
-GET https://localhost:8443/securePingApi/api/ping/whoAmI, returned 200/OK hdrs sent: [Accept=text/plain, Authorization=Basic dXNlcjE6cGFzc3dvcmQxIQ==] hdrs rcvd: [Connection=keep-alive, Content-Length=5, Content-Type=text/plain;charset=UTF-8, Date=Tue, 13 Nov 2018 14:43:27 GMT]
Java clients require server or server's certificate authority (CA) in trustStore
HTTPS will fail without Wildfly certificate in client trustStore
Define a path to Java Keystore with Server Public Key
# IT client pom.xml
<properties>
<java.truststore>${jboss.home}/standalone/configuration/application.keystore</java.truststore>
Example uses server's identity keyStore for simplicity
keyStore contains server's public key (and private key)
no password required to access keyStore public keys
Pass trustStore System Property using failsafe configuration
# IT client pom.xml
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<javax.net.ssl.trustStore>${java.truststore}</javax.net.ssl.trustStore>
</systemPropertyVariables>
</configuration>
...
results in process launched with -Djavax.net.ssl.trustStore=.../application.keystore
Java requires the common name (CN) of certificate from trustStore to match the hostname returned from server
Development scenarios can cause some mis-matches
Certificate generated for localhost used externally (expected localhost or 127.0.0.1)
Certificate generated for external used internally (expected xyz.com)
Can resolve thru Hostname Verifier Override
# IT client
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
# IT client
private static class MyHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
return true; //or whatever you wish to check for
}
}
Register verifier with ClientBuilder used to create JAX-RS Client
# IT client
ClientBuilder clientBuilder = ClientBuilder.newBuilder()
.hostnameVerifier(new MyHostnameVerifier());
Client jaxRsClient = clientBuilder.build();
Results in connection being established if certificate recognized - even if hostname mismatch
@Path("secured")
public Pinger authenticated() {
return new Pinger();
}
@Path("unsecured")
public Pinger anonymous() {
return new Pinger();
}
public class Pinger {
@Path("pingUser")
@GET
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Response pingUser() {
ResponseBuilder rb = null;
try {
PingResult result = makeResourcePayload(secureService!=null ?
secureService.pingUser() : "no ejb injected!!!");
rb = secureService!=null ?
Response.ok(result) :
Response.serverError().entity(result);
} catch (EJBAccessException ex) {
PingResult entity = makeResourcePayload(ex.toString());
rb = Response.status(Status.FORBIDDEN).entity(entity);
} catch (Exception ex) {
rb=makeExceptionResponse(ex);
}
return rb.build();
}
Define role restrictions for url-patterns
# web.xml
<security-constraint>
<web-resource-collection>
<web-resource-name>secured</web-resource-name>
<url-pattern>/api/ping/secured/pingUser</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
/api/ping/secured/pingUser constrained to callers with "user" role
CONFIDENTIAL transport-guarantee left off for example simplicity
Response
200/OK - if credentials supplied, successfully authenticated, and authorized
403/FORBIDDEN - if credentials supplied, successfully authenticated, but not authorized
401/UNAUTHORIZED - if not successfully authenticated
No web access control, delegating to EJB - Resource method called for all callers
GET https://localhost:8443/securePingApi/api/ping/pingUser
Web API access control - Resource method only invoked for authorized callers
GET https://localhost:8443/securePingApi/api/ping/secured/pingUser
Lock down access to web pages and commands
Identify user prior to EJB interaction
HTTP Basic Authentication
supported by HTTP protocol
username/password-based
browser collects information from client
authenticates user in a realm
not secure; passwords sent simple base64 encoding
short-comings overcome by layering over SSL (HTTPS)
Form-based Authentication
permits JSP/HTML forms to gather user info
HTTPS
encrypts communication channel
based on public/private key
hides all client/server exchange, including username/password info
Figure 117.1. Example Security Constraints
<security-constraint>
<web-resource-collection>
<web-resource-name>admin-only</web-resource-name>
<url-pattern>/model/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
Anything accessed via specified url-pattern must have admin role-name
Communication must be encrypted (i.e., switch to HTTPS)
Figure 117.2. Example FORM-based Login
<login-config>
<!--
<auth-method>BASIC</auth-method>
-->
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/WEB-INF/content/Login.jsp</form-login-page>
<form-error-page>/WEB-INF/content/Login.jsp</form-error-page>
</form-login-config>
</login-config>
Obtain missing user credentials using FORM when navigating to protected urls
Figure 117.3. Example Protected Servlet Mapping
<servlet>
<servlet-name>Handler</servlet-name>
<servlet-class>info.ejava.examples.secureping.web.SecurePingHandlerServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Handler</servlet-name>
<url-pattern>/model/admin/handler</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Handler</servlet-name>
<url-pattern>/model/user/handler</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Handler</servlet-name>
<url-pattern>/model/known/handler</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Handler</servlet-name>
<url-pattern>/model/handler</url-pattern>
</servlet-mapping>
Servlet accessible via multiple URLs
The example creates a security hole on purpose to be able to demonstrate EJB security backs the WEB security. The servlet mapped above is accessible through multiple URLs -- each restricted differently but attempting to provide the same functionality. If you access the servlet through the anonymous URL you will encounter many access failures communicating with the EJB. If you access the servlet using the admin URL you will be able to access all functionality.
Figure 117.4. WEB-INF/jboss-web.xml: security-domain
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss-web PUBLIC
"-//JBoss//DTD Web Application 2.4//EN"
"http://www.jboss.org/j2ee/dtd/jboss-web_4_0.dtd">
<jboss-web>
<security-domain>other</security-domain>
</jboss-web>
Assigning web-tier to same security-domain as EJB tier
Figure 117.5.
<html>
<body>
<h1>Login Required</h1>
<form action="j_security_check" method="POST">
User Name:
<input type="text" size="20" name="j_username">
Password:
<input type="password" size="10" name="j_password">
<input type="submit" value="Login">
</table>
</form>
</body>
</html>
form action j_security_check
is a standard container action for login forms
j_username
input field name standard for username
j_password
input field name standard for password
uri-path constrained to use only HTTPS
EJB returning formatted string with caller role information
Caller authenticated at web-tier and passed to EJB
EJB and WAR using same security-domain
BASIC authentication requested for any missing credentials
URL selected also not enforcing confidential (i.e., HTTPS)
Authenticate user passed to EJB
EJB rejects unauthorized caller
Table of Contents
At the completion of this topic, the student shall
have more understanding of:
Decouple implementation concerns using interceptors
Implement method interpose thru interceptors
be able to:
Implement an interceptor
Register/activate interceptor using EJB techniques
Register/activate interceptor using CDI techniques
Interpose on contructor, EJB business and lifecycle methods
Interceptor lifecycle is same as the bean it interposes on (i.e., Stateless/Stateful)
EJB Interceptors may define AroundInvoke
callbacks for
business methods
MDB onMessage()
May throw runtime and application exceptions declared by business method
Interceptors may rollback the current transaction by
throwing a RuntimeException
calling setRollbackOnly()
on the EJBContext
@Stateless
public class ContactsEJB implements ContactsRemote {
@PersistenceContext(unitName="ejbinterceptor-contacts")
private EntityManager em;
@Override
public Contact createContact(Contact contact) throws InvalidParam {
logger.debug("createContact({})", contact);
em.persist(contact);
return contact;
}
import javax.interceptor.AroundConstruct;
public class LifecycleInterceptor {
@Resource
private EJBContext ejbCtx;
@AroundInvoke
public Object invoke(InvocationContext ctx) throws Exception {
logger.debug("*** Business Method: {}, caller={}", ctx.getMethod(), ejbCtx.getCallerPrincipal().getName());
Object response = ctx.proceed();
logger.debug("*** Response Object: {}", response);
return response;
}
Simple interceptor logs call and response
LifecycleInterceptor] (EJB default - 4) *** Business Method: public info.ejava.examples.ejb.interceptor.bo.Contact info.ejava.examples.ejb.interceptor.ejb.ContactsEJB.createContact(info.ejava.examples.ejb.interceptor.bo.Contact) throws info.ejava.examples.ejb.interceptor.ejb.InvalidParam, caller=anonymous ContactsEJB] (EJB default - 4) createContact(Contact [id=0, name=John Doe, normalizedName=null]) LifecycleInterceptor] (EJB default - 4) *** Response Object: Contact [id=1, name=John Doe, normalizedName=john doe]
Interceptor was able to identify what was being called and who was calling it
Interceptor could have implemented security constraints, validation, etc.
Information about the call intercepted
Methods to control behavior of invocation chain
public interface javax.interceptor.InvocationContext {
public abstract java.lang.Object getTarget();
public abstract java.lang.Object getTimer();
public abstract java.lang.reflect.Method getMethod();
public abstract java.lang.reflect.Constructor<?> getConstructor();
public abstract java.lang.Object[] getParameters();
public abstract void setParameters(java.lang.Object[]);
public abstract java.util.Map<java.lang.String, java.lang.Object> getContextData();
public abstract java.lang.Object proceed() throws java.lang.Exception;
}
All EJB Interceptors may define standard lifecycle callbacks
AroundConstruct
PostConstruct
PreDestory
Stateful EJB Interceptors
may define state callbacks
PrePassivate
PostActivate
may use an extended persistence context
EJB Interceptors may define AroundTimeout
for timer methods
May call InvocationContext.getTimer()
to get timed out timer
May not throw application exceptions
Three (3) ways:
EJB Deployment descriptor (ejb-jar.xml)
@Interceptors annotation
CDI Deployment descriptor (beans.xml)
<?xml version="1.0"?>
<ejb-jar ...
<assembly-descriptor>
<interceptor-binding>
<ejb-name>ContactsEJB</ejb-name>
<interceptor-class>
info.ejava.examples.ejb.interceptor.interceptors.LifecycleInterceptor
</interceptor-class>
</interceptor-binding>
</assembly-descriptor>
</ejb-jar>
Decouples EJB and Interceptor classes
Assign to specific EJBs or globally
Assign to specific EJB methods or global to class
Can control order applied
//way 1 - class level
@Stateless
@Interceptors({ //global to EJB class
PreNormizedInterceptor.class,
ContactsNormalizerInterceptor.class,
PostNormizedInterceptor.class,
})
public class ContactsEJB implements ContactsRemote {
//way 2 - method level
@Stateless
public class ContactsEJB implements ContactsRemote {
@Override
@Interceptors({ //specific to individual EJB method
PreNormizedInterceptor.class,
ContactsNormalizerInterceptor.class,
PostNormizedInterceptor.class,
})
public Contact createContact(Contact contact) throws InvalidParam {
@Annotation near-equivalent to EJB deployment descriptor
Couples EJB class to Interceptor class
Cannot assign globally to all EJBs
Can assign to specific EJB methods or global to class
Can control order applied
Extends Java Interceptors specified in EJB Spec
Not specific to EJBs -- any POJO
Annotation to identify type of interceptor required (e.g., @Transactional)
package info.ejava.examples.ejb.interceptor.interceptors;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.interceptor.InterceptorBinding;
@Inherited
@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Validation {
}
The above @InterceptorBinding annotation will be used to designate the type of interceptor required/offered by/to a bean
Same implementation as EJB Interceptor
Annotated with @javax.interceptor.Interceptor
Annotated with @InterceptorBinding annotation
Not activate by default
import javax.interceptor.Interceptor;
@Validation
@Interceptor
public class ContactsNormalizerInterceptor {
@AroundInvoke
public Object invoke(InvocationContext ctx) throws Exception {
Can be an EJB
Annotated with @InterceptorBinding annotation
@Stateless
@Validation
public class ContactsEJB implements ContactsRemote {
@Override
public Contact createContact(Contact contact) throws InvalidParam {
<?xml version="1.0" encoding="UTF-8"?>
<beans ...
<interceptors>
<class>info.ejava.examples.ejb.interceptor.interceptors.PreNormizedInterceptor</class>
<class>info.ejava.examples.ejb.interceptor.interceptors.ContactsNormalizerInterceptor</class>
<class>info.ejava.examples.ejb.interceptor.interceptors.PostNormizedInterceptor</class>
</interceptors>
</beans>
Activates Interceptor classes
Controls order applied
EJB Bean Class
Business method(s)
Normalization and validation concerns
Decouple data manipulation/validation from business/DAO logic
Figure 120.1. EJB Bean with Business Method
@Stateless
@Validation
public class ContactsEJB implements ContactsRemote {
@Override
public Contact createContact(Contact contact) throws InvalidParam {
logger.debug("createContact({})", contact);
em.persist(contact);
return contact;
}
Must correct prior to processing
Callers can provide names using different normalization
Name could be missing entirely
Declarative API to define validation rules
JPA validates Default.class
group by default on @PrePersist and @PreUpdate
Can define other custom lifecycle groups
Figure 120.3. Validation API
@Entity
public class Contact implements Serializable {
@Id @GeneratedValue
@Column(name="CONTACT_ID")
private int id;
@NotNull(groups={PreNormalizedCheck.class, PrePersistCheck.class})
@Size(min=1, max=32, groups={PostNormalizedCheck.class, PrePersistCheck.class})
@Pattern.List({
@Pattern(regexp="^[A-Za-z0-9-\\ ]+$", groups=PreNormalizedCheck.class),
@Pattern(regexp="^([A-Z][a-z0-9-]+\\ *)+$", groups=PostNormalizedCheck.class)
})
@Column(name="CONTACT_NAME", length=32, nullable=false)
private String name;
@Valid
@OneToMany(fetch=FetchType.LAZY, mappedBy="contact", orphanRemoval=true,
cascade={CascadeType.PERSIST, CascadeType.REMOVE})
private List<ContactInfo> contactInfo;
Figure 120.4. Custom Validation Groups
public interface PreNormalizedCheck {}
public interface PrePersistCheck {}
public interface PostNormalizedCheck {}
JPA will validate Default
group for pre-persist and pre-update by default when activated
Can override/specify pre-persist, pre-update, and pre-remove validation
Figure 120.5. Activate JPA Validation with Custom Group
@NotNull(groups={PrePersistCheck.class})
@Size(min=1, max=32, groups={PrePersistCheck.class})
@Column(name="CONTACT_NAME", length=32, nullable=false)
private String name;
<?xml version="1.0" encoding="UTF-8"?>
<persistence ...
<persistence-unit name="ejbinterceptor-contacts">
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
<validation-mode>CALLBACK</validation-mode>
<properties>
<property name="javax.persistence.validation.group.pre-persist"
value="info.ejava.examples.ejb.interceptor.bo.PrePersistCheck"/>
<property name="javax.persistence.validation.group.pre-update"
value="info.ejava.examples.ejb.interceptor.bo.PrePersistCheck"/>
</properties>
</persistence-unit>
</persistence>
Activating validation within JPA lifecycle
Overriding/specifying pre-persist and pre-update to use custom PrePersistCheck
Custom group to defining validation after normalization and prior to JPA
Figure 120.6. PostNormalizedCheck
@Size(min=1, max=32, groups={PostNormalizedCheck.class, PrePersistCheck.class})
@Pattern(regexp="^([A-Z][a-z0-9-]+\\ *)+$", groups=PostNormalizedCheck.class)
private String name;
We apply detailed validation checks during PostNormalizationCheck
Custom group to defining validation prior to normalization
Figure 120.7. PreNormalizedCheck
@NotNull(groups={PreNormalizedCheck.class, PrePersistCheck.class})
@Size(min=1, max=32, groups={PostNormalizedCheck.class, PrePersistCheck.class})
@Pattern.List({
@Pattern(regexp="^[A-Za-z0-9-\\ ]+$", groups=PreNormalizedCheck.class),
@Pattern(regexp="^([A-Z][a-z0-9-]+\\ *)+$", groups=PostNormalizedCheck.class)
})
@Column(name="CONTACT_NAME", length=32, nullable=false)
private String name;
We apply brief sanity check validation prior to normalization
Handle validating Validation API-marked POJOs as Interceptor
Figure 120.8. Validator Base Class
public class ValidatorInterceptor {
@Inject
private Validator validator;
private Class<?>[] groups;
protected ValidatorInterceptor() {}
public ValidatorInterceptor(Class<?>[] groups) { this.groups = groups; }
@AroundInvoke
public Object invoke(InvocationContext ctx) throws Exception {
logger.debug("validating method: {}, groups: {}", ctx.getMethod(), Arrays.toString(groups));
//validate each parameter
for (Object param: ctx.getParameters()) {
logger.debug("validating param: {}, groups: {}", param, Arrays.toString(groups));
Set<ConstraintViolation<Object>> violations = validator.validate(param, groups);
if (!violations.isEmpty()) {
Exception ex = new InvalidParam(param.toString(), getErrors(violations));
logger.debug("aborting call, found error: {}", ex.getMessage());
throw ex;
}
}
return ctx.proceed();
}
private List<String> getErrors(Set<ConstraintViolation<Object>> violations) {
List<String> errors = new ArrayList<String>(violations.size());
for (ConstraintViolation<Object> v: violations) {
errors.add(v.toString());
}
return errors;
}
}
Grabs parameters from InvocationContext
Validates each of them against assigned validation group
Figure 120.9. Pre/PostNormalized Concrete Interceptors
@Validation
@Interceptor
public class PreNormizedInterceptor extends ValidatorInterceptor {
public PreNormizedInterceptor() {super(new Class<?>[]{PreNormalizedCheck.class});}
}
@Validation
@Interceptor
public class PostNormizedInterceptor extends ValidatorInterceptor {
public PostNormizedInterceptor() { super(new Class<?>[]{PostNormalizedCheck.class}); }
}
Define @InterceptorBinding (@Validation) and @Interceptor role for CDI
Define validation group(s) for base class
Intercepts parameters going into business methods
Applies normalization rules to parameter values
Figure 120.10. ContactsNormalizerInterceptor
@Validation
@Interceptor
public class ContactsNormalizerInterceptor {
private ContactNormalizer contactNormalizer=new ContactNormalizer();
@AroundInvoke
public Object invoke(InvocationContext ctx) throws Exception {
logger.debug("intercepting: {}", ctx.getMethod());
for (Object param: ctx.getParameters()) {
if (param instanceof Contact) {
logger.debug("normalizing: {}", param);
contactNormalizer.normalize((Contact) param);
logger.debug("normalized: {}", param);
} else if (param instanceof PostalInfo) {
logger.debug("normalizing: {}", param);
contactNormalizer.normalize((PostalInfo) param);
logger.debug("normalized: {}", param);
} else if (param instanceof PhoneInfo) {
logger.debug("normalizing: {}", param);
contactNormalizer.normalize((PhoneInfo) param);
logger.debug("normalized: {}", param);
}
}
return ctx.proceed();
}
Defines @InterceptorBinding (@Validator) and @Interceptor role
Defines @AroundInvoke for business method
Subjects each parameter to normalization rules
Details of normalization omitted (initial caps for each name/word)
Figure 120.11. Bean Class
@Stateless
@Validation
public class ContactsEJB implements ContactsRemote {
@Override
public Contact createContact(Contact contact) throws InvalidParam {
logger.debug("createContact({})", contact);
em.persist(contact);
return contact;
}
Applies @interceptorBinding annotation (@Validation)
Supplies business method
One of several techniques possible
Figure 120.12. CDI Activation (bean.xml)
<?xml version="1.0" encoding="UTF-8"?>
<beans ...
<interceptors>
<class>info.ejava.examples.ejb.interceptor.interceptors.PreNormizedInterceptor</class>
<class>info.ejava.examples.ejb.interceptor.interceptors.ContactsNormalizerInterceptor</class>
<class>info.ejava.examples.ejb.interceptor.interceptors.PostNormizedInterceptor</class>
</interceptors>
</beans>
Control ordering of interceptors
Figure 120.13. Example Output
ValidatorInterceptor] validating param: Contact [id=0, name=john Doe], groups: [interface info.ejava.examples.ejb.interceptor.bo.PreNormalizedCheck] ContactsNormalizerInterceptor] normalizing: Contact [id=0, name=john Doe] ContactsNormalizerInterceptor] normalized: Contact [id=0, name=John Doe] ValidatorInterceptor] validating param: Contact [id=0, name=John Doe], groups: [interface info.ejava.examples.ejb.interceptor.bo.PostNormalizedCheck] ContactsEJB] createContact(Contact [id=0, name=John Doe]) call next value for hibernate_sequence insert into EJBINTERCEPTOR_CONTACT (CONTACT_NAME, NORMALIZED_NAME, CONTACT_ID) values (?, ?, ?)
Input hand a correctable, but invalid name
Name passed initial pre-normalization validation
Name corrected by normalization
Name passed post-normalization validation
Name passed pre-persist validation
Contac persisted
Table of Contents
Understand concepts behind "messaging"
Introduce core aspects of the JMS 2.0 API
Implement asynchronous messaging flows within an application
At the completion of this topic, the student shall be able to:
Describe the difference between messaging and a standard client/server call
Describe the difference between publish/subscribe (Topic) and send/receive (Queue) messaging
Define administered resources in the server
Lookup administered resources using JNDI in their JavaSE-based applications
Inject administered resources in their JavaEE container-based applications
Construct runtime resources within their application
Send and receive JMS messages
Communication between applications that exchange messages
Message forms a single, encapsulated, unit of communication between applications
De-couples Producer and Consumer of the Message
Message-Oriented-Middleware (MOM)
category of application communication
uses asynchronous message passing versus synchronous request/reply
Advantages
Producer and Consumer operate independently
Messages can be persisted when consumer unavailable
Messages can be retrieved even after producer is unavailable
Qualities of service can be applied independent of clients
Resource utilization can be applied by messaging provide
Producer/Consumer communicate over a TCP/IP connection
Directly aware of state of link to the other end
Producer cannot send messages when Consumer unavailable
Consumer cannot receive messages when Producer unavailable
Separate messages must be sent to separate clients
Producer must be aware of what Consumer needs
Security and QoS must be implemented by entirely within clients
Good for realtime status networks
Producer/Consumer communicate over a Uni-cast IP
No concept of an end-to-end link
Producer issues messages whether Consumer available or not
MOM can add acknowledgment and queuing mechanisms
Consumer cannot receive messages when Producer unavailable
Producer only needs to send one message
Consumers able to control when they receive
Security and QoS must be implemented by entirely within clients
Good for high performance publish/subscribe networks
Producer/Consumer communicate to a separate broker
No concept of an end-to-end link
Producer issues messages whether Consumer available or not
application can add acknowledgment mechanisms
Consumer can receive messages when Producer unavailable
Producer only needs to send one message
Consumers able to control when they receive
Security and QoS can be implemented within broker (thin client)
Good for fully decoupling messaging complexity from clients
Clients unaware of physical topology
MOM Brokers link can
honor time-of-day bandwidth constraints
conserve bandwidth between sites by only sending what is needed
point of site-to-site firewall control
form alternate route routing
Producer
produces message
Destination
target of produced message
source of consumed message
hosted by messaging provider
Consumer
consumes message
Sender
a Producer
sends message to a Queue with specific target/intent
Queue
a Destination
delivers message to, at most, one receiver
Receiver
a Consumer
intended target of message
Publisher
a Producer
publishes message to Topic with no specific target/intent
Topic
a Destination
delivers message to all Subscribers
Subscriber
a Consumer
has registered interest in a Topic
durable subscription – lives beyond active client connection
non-durable subscription – only exists during client connection
Requestor
sends message to a destination appropriate to be available to a Replier
receives reply for request
Request Destination
can be Topic or Queue
Reply Destination
can be Topic or Queue (typically a Queue)
Replier
receives request message from destination
sends reply message to destination specified in request
Each interaction with Destination can be made part of an existing ACID transaction
Transaction #1
Requestor
begins some work
sends message to a destination appropriate to be available to a Replier
Transaction #2
Replier
receives request message from destination
performs work
sends reply message to destination specified in request
Transaction #3
Requestor
receives reply for request
completes work
Vendor-neutral API to access enterprise messaging systems.
Similar API role as JDBC
JDBC is an API for accessing RDBMS
JMS is an API for accessing enterprise messaging systems
API between the application (JMS client) and the messaging provider (JMS provider); not between providers
Similar non-role as JDBC
JDBC won't cause data inserted into an HSQL instance to magically show up in an Oracle instance
JMS won't cause a message sent to a JBossMQ destination to magically show up in a BEA instance's destination
Its a Java API; no other languages addressed
JMS providers accommodate other language clients using proprietary non-Java APIs
Security
How are destinations secured
Load Balancing/Fault Tolerance
How do Topics scale to many publishers/subscribers
How does a provider account for broker failure
Error Notifications
What happens when storage exhausted
FIFO?, LIFO? retention
Administration
How are destinations and connection factories added
Message Repository
How is storage allocated
Wire Protocol
RMI?
HTTP?
Other?
Interoperability with non-Java clients
1.0
~1998-2001
Initial API
Very destination-type specific, hard to generalize
publisher.publish() to Topic
sender.send() to Queue
1.1
2002
Generalized destination-type specific calls
producer.send() to Topic or Queue
consumer.receive() from Topic or Queue
2.0
2013
Ease of use
Combined Connection and Session into JMSContext
Fewer objects/calls to setup a flow
Java 7 AutoClosable eliminates boilerplate code in try/finally blocks
Read contents of Message without downcasting to Message type
New features
Shared subscriptions
Delivery delay
JMS 2.0 "Ease of Use"
Collapsed Connection and Session functionality into JMSContext
Created easier to manage/use JMSProducer and JMSConsumer
Encapsulates a set of properties usually assigned by administrator
clientId - optional unique identity required for durable subscriptions
listen address
commonly obtained thru JNDI lookup
import javax.naming.InitialContext;
import javax.jms.ConnectionFactory;
InitialContext jndi = new InitialContext();
ConnectionFactory connFactory=(ConnectionFactory)jndi.lookup("jms/RemoteConnectionFactory");
Client's primary interface to JMS provider
Combines Connection and Session functionality
Allocates resources outside of client JVM
Created from ConnectionFactory
import javax.jms.JMSContext;
JMSContext context = connFactory.createContext()
Must be closed (except when injected)
Supports JavaSE 7 AutoCloseable
try (JMSContext context = connFactory.createContext()) {
...
}
Authentication can be performed when created from JavaSE client
JMSContext context = connFactory.createContext(user, password)
Can be injected when running within JavaEE container
import javax.jms.JMSConnectionFactory;
@Inject @JMSConnectionFactory("java:/JmsXA")
private JMSContext jmsContext;
One thread per JMSContext
application may create child JMSContext for multi-threaded applications
try (JMSContext context=createContext();
JMSContext context2=context.createContext(JMSContext.CLIENT_ACKNOWLEDGE)) {
...
}
Supports an ExceptionListener
Works the same for Topic and Queue
JNDI lookups supported for JavaSE clients
Injection supported for JavaEE components
import javax.jms.Destination;
Destination destination = (Destination)jndi.lookup("jms/queue/ejava/examples/jmsMechanics/queue1")
Obtain JMSContext from ConnectionFactory (JavaSE) or injection (JavaEE container)
Obtain Destination from JNDI lookup (JavaSE) or injection (JavaEE container)
Create Message
Message - all properties and no payload
Messages have pre-defined and user-defined properties
Message properties are subject to Subscription Selectors in broker
Messages are not closed
MapMessage - Map payload of properties
Additional properties opaque to broker
Properties required to be of "java.lang" types - no custom types
TextMessage - textual payload
Ideal for XML and JSON-based DTO payloads
BytesMessage - binary blob payload
ObjectMessage - Java serialized object payload
Requires Java-based receiver with DTO Classes
StreamMessage - sequential data payload
import javax.jms.MapMessage;
MapMessage message = jmsContext.createMapMessage();
Set Message Properties (optional)
Message property accessesors throw JMSException checked exceptions
import javax.jms.JMSException;
message.setJMSType("saleUpdate"); //JMSType is a pre-defined property
message.setStringProperty("awayClub", ...);//"awayClub" is example of user-defined String property
message.setIntProperty("awayTeamId", ...); //"awayTeamId" is example of user-defined int property
Set Message Payload
//this example is a MapMessage - each Message type has specific interface
message.setLong("id", item.getId());
message.setString("name", item.getName());
message.setString("seller", item.getOwner().getUserId());
message.setLong("startDate", item.getStartDate().getTime());
message.setLong("endDate", item.getEndDate().getTime());
message.setDouble("minBid", item.getMinBid());
message.setDouble("bids", item.getBids().size());
message.setDouble("highestBid",
(item.getHighestBid() == null ? 0.00 : item.getHighestBid().getAmount()));
Create JMSProducer
JMSProducers are not closed - different from JMS 1.1 Producer
import javax.jms.Producer;
JMSProducer producer = context.createProducer();
Set JMSProducer options
Time to live - number of millisecs a sent message will live in the broker waiting to be delivered
Priority - delivery priority of messages given to broker
Delivery Mode - level of guarantee required when completing a send() to the broker
Delivery Delay - minimum number of millsecs before broker will issue to consumer
//JMSProducer supports method chaining
producer.setPriority(Message.DEFAULT_PRIORITY)
.setTimeToLive(Message.DEFAULT_TIME_TO_LIVE)
.setDeliveryMode(Message.DEFAULT_DELIVERY_MODE)
.setDeliveryDelay(Message.DEFAULT_DELIVERY_DELAY);
Send Message to Destination
producer.send(sellTopic, message);
Obtain JMSContext from ConnectionFactory (JavaSE) or injection (JavaEE container)
Obtain Destination from JNDI lookup (JavaSE) or injection (JavaEE container)
Create a JMSConsumer for a specific destination
JMSConsumer must be closed and is AutoCloseable
import javax.jms.JMSConsumer;
try (JMSConsumer syncConsumer = context.createConsumer(destination);
JMSConsumer asyncConsumer = context2.createConsumer(destination)) {
}
Call receive() to obtain the next message
receive() - perminently wait for next Message
receiveNoWait() - immediately return with next Message or null if none waiting
receive(long timeout) - wait up to timeout msecs before returning null if no Message
Message message=consumer.receiveNoWait();
Get Message Properties (optional)
String level = message.getStringProperty("level");//"level" is user-defined property
logger.debug("receive ({}, mode={}, pri={}{}):{}",++count,
message.getJMSDeliveryMode(), //"JMSDeliveryMode" is pre-defined property
message.getJMSPriority(), //"JMSPriority" is pre-defined property
(level==null?"":", level="+level),
message.getJMSMessageID()); //"JMSMessageID" is pre-defined property
Get Message Payload
TextMessage m1 = ...
TextMessage text = m1.request.getText();
ObjectMessage m2 = ...
XxxDTO dto = (XxxDTO)m2.getObject();
Alternately, call JMS 2.0 receiveBody(T) to get just the payload
message.receiveBody(Class<T> c)
message.receiveBodyNoWait(Class<T> c)
message.receiveBody(Class<T> c, long delay)
Obtain JMSContext from ConnectionFactory (JavaSE)
Obtain Destination from JNDI lookup (JavaSE)
Implement javax.jms.MessageListener
public class AsyncClient implements MessageListener ... {
public void onMessage(Message message) {
...
}
}
Create a JMSConsumer
JMSConsumer must be closed and implements AutoCloseable
try (JMSConsumer syncConsumer = context.createConsumer(destination);
JMSConsumer asyncConsumer = context2.createConsumer(destination)) {
...
}
Register MessageListener with JMSConsumer
//create a client to asynchronous receive messages through onMessage() callbacks
AsyncClient asyncClient = new AsyncClient();
asyncConsumer.setMessageListener(asyncClient);
Process Messages as they are received through onMessage() callback
Implement a javax.jms.MessageListener
public class BuyerMDB implements MessageListener {
public void onMessage(Message message) {
Annotate the class as @MessageDriven
Fill in @MessageDriven @ActivationConfigProperty values
destinationLookup - JNDI name for destination
destinationType - javax.jms.Queue or javax.jms.Topic
messageSelector - SQL-like property selector
acknowledgeMode - when to 100% accept message from broker
subscriptionDurability - durable or transient Topic subscription
clientId - unique JMSContext/Connection ID for durable subscription
subscriptionName - required for durable subscription
connectionFactoryLookup - JNDI name for ConnectionFactory
@MessageDriven(activationConfig={
@ActivationConfigProperty(
propertyName="destinationType",
propertyValue="javax.jms.Topic"),
@ActivationConfigProperty(
propertyName="destination",
propertyValue="java:/jms/topic/ejava/examples/asyncMarket/topic1"),
@ActivationConfigProperty(
propertyName="messageSelector",
propertyValue="JMSType in ('forSale', 'saleUpdate')"),
@ActivationConfigProperty(
propertyName="acknowledgeMode",
propertyValue="Auto-acknowledge")
})
public class BuyerMDB implements MessageListener {
@PermitAll
public void onMessage(Message message) {
Grant container permission to invoke onMessage()
Annotate class or onMessage() method with @PermitAll
@MessageDriven(...)
public class BuyerMDB implements MessageListener {
@PermitAll
public void onMessage(Message message) {
Process Messages as they are received through onMessage() callback
Broker retains consumed until acknowledged
Smaller windows (ack per Message) slower but can keep client and broker fully synchronized
Larger windows (ack per N Messages) faster but can produce duplicate messages
try (JMSContext context = connFactory.createContext(JMSContext.AUTO_ACKNOWLEDGE)) {
...
Message message = ...
}
Message automatically acknowledged by session when client synchronously receives (receive()) or asynchronously processes (onMessage) message
Can be used with MessageDriven EJBs (MDBs)
@MessageDriven(activationConfig={
@ActivationConfigProperty(
propertyName="acknowledgeMode",
propertyValue="Auto-acknowledge")
})
public class BuyerMDB implements MessageListener {
@PermitAll
public void onMessage(Message message) {
...
}
}
try (JMSContext context = connFactory.createContext(JMSContext.DUPS_OK_ACKNOWLEDGE)) {
...
Message message = ...
}
Similar to AUTO_ACKNOWLEDGE
Session lazily acknowledges messages
Can result in duplicate messages
Can also be used with MessageDriven EJBs (MDBs)
@MessageDriven(activationConfig={
@ActivationConfigProperty(
propertyName="acknowledgeMode",
propertyValue="Dups-ok-acknowledge")
})
public class BuyerMDB implements MessageListener {
@PermitAll
public void onMessage(Message message) {
...
}
}
try (JMSContext context = connFactory.createContext(JMSContext.CLIENT_ACKNOWLEDGE)) {
...
Message message = ...
message.acknowledge();
}
Messages are manually acknowledged
Any acknowledged message acknowledges all prior messages consumed
try (JMSContext context = connFactory.createContext(JMSContext.SESSION_TRANSACTED)) {
...
Message message = ...
context.commit();
}
Send or receive Message(s) within a local transaction
Changes are not reflected in broker until commit() and undone on call to rollback()
Useful for JavaSE clients to receive and fully process message
Can only be used with MDBs using Bean-Managed Transactions
Dependency Management
<properties>
<javax.jms-api.version>2.0.1</javax.jms-api.version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>javax.jms</groupId>
<artifactId>javax.jms-api</artifactId>
<version>${javax.jms-api.version}</version>
</dependency>
Dependencies
<dependencies>
<dependency>
<groupId>javax.jms</groupId>
<artifactId>javax.jms-api</artifactId>
<scope>provided</scope>
</dependency>
Dependency Management
<properties>
<wildfly-naming-client.version>1.0.9.Final</wildfly-naming-client.version>
<artemis-jms.version>1.5.5</artemis-jms.version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.wildfly</groupId>
<artifactId>wildfly-naming-client</artifactId>
<version>${wildfly-naming-client.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-jms-client</artifactId>
<version>${artemis-jms.version}</version>
</dependency>
Dependencies
<dependencies>
<dependency>
<groupId>org.wildfly</groupId>
<artifactId>wildfly-naming-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-jms-client</artifactId>
<scope>test</scope>
</dependency>
The test dependencies above should be automatically brought in through the info.ejava.examples.common:jboss-rmi-client dependency used by many IT tests.
"What's New in JMS 2.0", Nigel Deakin, https://www.oracle.com/technetwork/articles/java/jms20-1947669.html, 2013 Oracle Technology Network
Table of Contents
At the completion of this topic, the student shall be able to:
Define an EJB method to complete in an asynchronous thread
Invoke an asynchronous method
Check on the completion status of an asynchronous method
Return a result from an asynchronous method
Compare use of asynchronous methods versus JMS
Task(s) may take considerable time to complete
Client may not need to wait for task to complete
Multiple tasks could run concurrently
Client may gather results as they complete
Manually create a Java thread to complete task
Messages are processed by JMS consumers in independent threads
Establish
a JMS destination for work requests
a JMS destination for work responses
a MessageDriven Bean to perform asynchronous work
Client
builds JMS message with request information
sends JMS request message to request destination
continues working
waits for response
MessageDriven Bean
is invoked to handle the JMS request message
performs requested work
builds JMS message with response information
sends JMS response message to response destination
Client
receives JMS response message
extracts the response information
Request container to execute method in independent thread from caller
Client and called method are fully decoupled from one another
Called method executes in new transaction context
Available since EJB 3.1
Establish
an EJB method to perform work in independent thread
Annotate EJB method with @javax.ejb.Asynchronous
Alternately annotate EJB class to make all methods asynchronous
Declare return type as:
void or
java.util.concurrent.Future
import java.util.concurrent.Future;
import javax.ejb.Asynchronous;
@Stateless
public class AuctionMgmtActionEJB {
@Asynchronous
public Future<Date> doWorkAsync(long delay) {
Asynchronous methods that return "void" cannot throw application exceptions. Asynchronous methods that return "Future<T> can throw application exceptions.
Client
Invokes method normally using standard arguments
Future<Date> f = actions.doWorkAsync(delay);
Asynchronous Method
performs work
returns result information wrapped in javax.ejb.AsyncResult
javax.ejb.AsyncResult
@Asynchronous
public Future<Date> doWorkAsync(long delay) {
Date now = ...
return new AsyncResult<Date>(now);
}
AsyncResult is only a convenience class used to pass the result to the container. This instance is not accessible to the client. The client will be given an instance of a Future built and managed by the container.
Client
immediately receives Future<T> response message
may call isDone() to check on status
Future<Date> f = actions.doWorkAsync(delay);
...
f.isDone();
may call get() to check on wait for result
Future<Date> f = actions.doWorkAsync(delay);
...
Date date = f.get();
Client EJB
@Stateless
public class AuctionMgmtEJB implements AuctionMgmtRemote, AuctionMgmtLocal {
@EJB
private AuctionMgmtActionEJB actions;
Worker EJB
@Stateless
public class AuctionMgmtActionEJB {
public Date doWorkSync(long delay) {
}
@Asynchronous
public Future<Date> doWorkAsync(long delay) {
}
/**
* Perform action synchronously while this caller waits
*/
@Override
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public void workSync(int count, long delay) {
DateFormat df = new SimpleDateFormat("HH:mm:ss.SSS");
long startTime = System.currentTimeMillis();
for (int i=0; i<count; i++) {
logger.info("{} issuing sync request, delay={}", df.format(new Date()), delay);
Date date = actions.doWorkSync(delay);
logger.info("sync waitTime={} msecs", System.currentTimeMillis()-startTime);
}
long syncTime = System.currentTimeMillis() - startTime;
logger.info("workSync time={} msecs", syncTime);
}
Invokes count synchronous workers sequentially
/**
* Perform action synchronously while caller waits.
*/
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public Date doWorkSync(long delay) {
DateFormat df = new SimpleDateFormat("HH:mm:ss.SSS");
logger.debug("sync method {} starting {} delay at {}",
Thread.currentThread().getId(), delay, df.format(new Date()));
try { Thread.sleep(delay); }
catch (Exception ex) {
logger.error("unexpected error during sleep", ex);
}
Date now = new Date();
logger.debug("sync method {} completed {} delay at {}",
Thread.currentThread().getId(), delay, df.format(now));
return now;
}
Work performed takes "delay" amount of time
Timestamp of completion printed and returned to caller when complete
08:01:04,614 [Client] (default task-2) 08:01:04.614 issuing sync request, delay=3000
08:01:04,614 [Worker] (default task-2) sync method 231 starting 3000 delay at 08:01:04.614
08:01:07,618 [Worker] (default task-2) sync method 231 completed 3000 delay at 08:01:07.617
08:01:07,618 [Client] (default task-2) sync waitTime=3004 msecs
08:01:07,619 [Client] (default task-2) 08:01:07.619 issuing sync request, delay=3000
08:01:07,619 [Worker] (default task-2) sync method 231 starting 3000 delay at 08:01:07.619
08:01:10,621 [Worker] (default task-2) sync method 231 completed 3000 delay at 08:01:10.620
08:01:10,621 [Client] (default task-2) sync waitTime=6007 msecs
08:01:10,621 [Client] (default task-2) 08:01:10.621 issuing sync request, delay=3000
08:01:10,622 [Worker] (default task-2) sync method 231 starting 3000 delay at 08:01:10.622
08:01:13,626 [Worker] (default task-2) sync method 231 completed 3000 delay at 08:01:13.625
08:01:13,626 [Client] (default task-2) sync waitTime=9012 msecs
08:01:13,627 [Client] (default task-2) workSync time=9013 msecs
Workers execute in same thread ("default task-2") as caller
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* Perform action async from this caller
*/
@Override
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public void workAsync(int count, long delay) {
DateFormat df = new SimpleDateFormat("HH:mm:ss.SSS");
List<Future<Date>> results = new ArrayList<Future<Date>>();
//issue requests
long startTime = System.currentTimeMillis();
for (int i=0; i<count; i++) {
logger.info("{} issuing async request, delay={}", df.format(new Date()), delay);
Future<Date> date = actions.doWorkAsync(delay);
results.add(date);
logger.info("async waitTime={} msecs", System.currentTimeMillis()-startTime);
}
//process results
for (Future<Date> f: results) {
logger.info("{} getting async response", df.format(new Date()));
try {
Date date = f.get();
} catch (ExecutionException | InterruptedException ex) {
logger.error("unexpected error on future.get()", ex);
throw new EJBException("unexpected error during future.get():"+ex);
}
logger.info("{} got async response", df.format(new Date()));
}
long asyncTime = System.currentTimeMillis() - startTime;
logger.info("workAsync time={} msecs", asyncTime);
}
Invokes count synchronous workers sequentially
/**
* Perform action async from caller
*/
@Asynchronous
public Future<Date> doWorkAsync(long delay) {
DateFormat df = new SimpleDateFormat("HH:mm:ss.SSS");
logger.debug("async method {} starting {} delay at {}",
Thread.currentThread().getId(), delay, df.format(new Date()));
try { Thread.sleep(delay); }
catch (Exception ex) {
logger.error("unexpected error during sleep", ex);
}
Date now = new Date();
logger.debug("async method {} completed {} delay at {}",
Thread.currentThread().getId(), delay, df.format(now));
return new AsyncResult<Date>(now);
}
Work performed takes "delay" amount of time
Timestamp of completion printed and returned to caller when complete
08:08:15,861 [Client] (default task-1) sellTopic=ActiveMQTopic[asyncMarket-topic1]
08:08:15,861 [Client] (default task-1) 08:08:15.861 issuing async request, delay=3000
08:08:15,863 [Client] (default task-1) async waitTime=2 msecs
08:08:15,863 [Client] (default task-1) 08:08:15.863 issuing async request, delay=3000
08:08:15,864 [Client] (default task-1) async waitTime=3 msecs
08:08:15,864 [Client] (default task-1) 08:08:15.864 issuing async request, delay=3000
08:08:15,864 [Client] (default task-1) async waitTime=3 msecs
08:08:15,864 [Client] (default task-1) 08:08:15.864 getting async response
08:08:15,865 [Worker] (EJB default - 8) async method 224 starting 3000 delay at 08:08:15.865
08:08:15,865 [Worker] (EJB default - 6) async method 222 starting 3000 delay at 08:08:15.865
08:08:15,865 [Worker] (EJB default - 7) async method 223 starting 3000 delay at 08:08:15.865
08:08:18,870 [Worker] (EJB default - 7) async method 223 completed 3000 delay at 08:08:18.870
08:08:18,870 [Worker] (EJB default - 8) async method 224 completed 3000 delay at 08:08:18.870
08:08:18,870 [Worker] (EJB default - 6) async method 222 completed 3000 delay at 08:08:18.870
08:08:18,873 [Client] (default task-1) 08:08:18.873 got async response
08:08:18,873 [Client] (default task-1) 08:08:18.873 getting async response
08:08:18,873 [Client] (default task-1) 08:08:18.873 got async response
08:08:18,873 [Client] (default task-1) 08:08:18.873 getting async response
08:08:18,873 [Client] (default task-1) 08:08:18.873 got async response
08:08:18,873 [Client] (default task-1) workAsync time=3012 msecs
Workers execute in different threads ("EJB default - #") from caller ("default task-1")
08:13:10,640 (AsyncMethodIT.java:29) -count=3, delay=3000, syncTime=9017 msecs
08:13:10,640 (AsyncMethodIT.java:30) -count=3, delay=3000, asyncTime=3018 msecs
Asynchronous approach was much faster than synchronous approach
Asynchronous approach allowed work to be done concurrently
"Java Platform, Enterprise Edition: The Java EE Tutorial, Asynchronous Method Invocation", Oracle, https://docs.oracle.com/javaee/7/tutorial/ejb-async001.htm, 2014 Oracle
Table of Contents
Perform similar role of job schedulers
e.g., cron
Three types
Single Action Timer
Interval Timer
Calendar Timer
Timers are specific to an EJB
Each EJB may have one @Timeout method but many timers
import javax.ejb.Timeout;
@Timeout
public void execute(Timer timer) {
Timer can hold Serializable context information
public long sellProduct(String sellerId, AuctionItem item) throws ResourceNotFoundException {
...
timerService.createTimer(item.getEndDate(), new Long(item.getId()));
timerService.createSingleActionTimer(item.getEndDate(),
new TimerConfig(new Long(item.getId()), false));
If you need multiple @Timeout method behaviors - create multiple EJBs
Original EJB Timer Service part of EJB 2.1
EJB 2.x is pre-annotations, pre-JavaSE 5, interface-based
EJB 3.0 added annotation-based extensions
eliminated interface-based solution requirements
EJB 3.1 provided overhaul of the overall service
Added cron-like, calendar schedules
import javax.ejb.ScheduleExpression;
public void initTimers(ScheduleExpression schedule) {
...
}
Added declarative scheduling
import javax.ejb.Schedule;
import javax.ejb.Timeout;
@Timeout
@Schedule(second="*/10", minute ="*", hour="*", dayOfMonth="*", month="*", year="*", persistent=false)
public void execute(Timer timer) {
import javax.annotation.Resource;
import javax.ejb.TimerService;
@Resource
private TimerService timerService;
Starting point for all EJB Timer programmatic manipulation
Timer Creation
createCalendarTimer(ScheduleExpression schedule, ...)
createIntervalTimer(Date initialExpiration, long intervalDuration, ...)
createSingleActionTimer(Date expiration, ...)
Get Timers
getTimers() - get Timers associated with this EJB
getAllTimers() - get Timers associated with this module
EJB Timers are created as perisistent=true by default. This sounds reasonable until you begin refactoring your application and start seeing "EJB not found", etc. on follow-on redeploys or many more EJB Timers firing that you believe should be. For programmatic EJB Timers - always pass in the optional TimerConfig and set persistent to false. For the Schedule annotation, set the persistent attribute to false.
It is always desirable to be able to easily coldstart your application in development with a reboot of the application server or a redeploy of the application and not have to worry about artifacts of a previous implementation approach.
Call @Timeout method after a specified expiration
createSingleActionTimer(Date expiration, TimerConfig timerConfig)
createSingleActionTimer(long duration, TimerConfig timerConfig)
Call @Timeout method after a given point in time and then specified duration after that
createIntervalTimer(Date initialExpiration, long intervalDuration, TimerConfig timerConfig)
createIntervalTimer(long initialDuration, long intervalDuration, TimerConfig timerConfig)
Call @Timeout method based on the schedule expression
second
minute
hour
dayOfMonth
month
dayOfWeek
year
createCalendarTimer(ScheduleExpression schedule, TimerConfig timerConfig)
@Timeout callback method annotated with @Schedule
@Timeout
@Schedule(second="*/10", minute ="*", hour="*",
dayOfMonth="*", month="*", year="*", persistent=false)
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void execute(Timer timer) {
logger.info("timer fired: {}", timer);
try {
checkAuction();
}
catch (Exception ex) {
logger.error("error checking auction", ex);
}
}
Server-side output
23:08:30 [AuctionMgmtEJB] (EJB default-6) **** AuctionMgmtEJB init() ***
23:08:30 [AuctionMgmtEJB] (EJB default-6) timerService=org.jboss.as.ejb3.timerservice.TimerServiceImpl@d72fc25
23:08:30 [AuctionMgmtEJB] (EJB default-6) checkAuctionInterval=10000
23:08:30 [AuctionMgmtEJB] (EJB default-6) timer fired: [id=02cbc8b5-0ddd-414e-b81d-59c7800371fd
timedObjectId=asyncMarketEAR.asyncMarketEJB.AuctionMgmtEJB auto-timer?:true persistent?:false
timerService=org.jboss.as.ejb3.timerservice.TimerServiceImpl@d72fc25
initialExpiration=null intervalDuration(in milli sec)=0
nextExpiration=Tue Dec 04 23:08:40 EST 2018 timerState=IN_TIMEOUT info=null]
23:08:30 [AuctionMgmtEJB] (EJB default-6) checking auctions
23:08:30 [AuctionMgmtEJB] (EJB default-6) processed 0 active items
23:08:40 [AuctionMgmtEJB] (EJB default-7) **** AuctionMgmtEJB init() ***
23:08:40 [AuctionMgmtEJB] (EJB default-7) timerService=org.jboss.as.ejb3.timerservice.TimerServiceImpl@d72fc25
23:08:40 [AuctionMgmtEJB] (EJB default-7) checkAuctionInterval=10000
23:08:40 [AuctionMgmtEJB] (EJB default-7) timer fired: [id=02cbc8b5-0ddd-414e-b81d-59c7800371fd
timedObjectId=asyncMarketEAR.asyncMarketEJB.AuctionMgmtEJB auto-timer?:true persistent?:false
timerService=org.jboss.as.ejb3.timerservice.TimerServiceImpl@d72fc25
initialExpiration=null intervalDuration(in milli sec)=0
nextExpiration=Tue Dec 04 23:08:50 EST 2018 timerState=IN_TIMEOUT info=null]
23:08:40 [AuctionMgmtEJB] (EJB default-7) checking auctions
23:08:40 [AuctionMgmtEJB] (EJB default-7) processed 0 active items
23:08:50 [AuctionMgmtEJB] (EJB default-9) **** AuctionMgmtEJB init() ***
23:08:50 [AuctionMgmtEJB] (EJB default-9) timerService=org.jboss.as.ejb3.timerservice.TimerServiceImpl@d72fc25
23:08:50 [AuctionMgmtEJB] (EJB default-9) checkAuctionInterval=10000
23:08:50 [AuctionMgmtEJB] (EJB default-9) timer fired: [id=02cbc8b5-0ddd-414e-b81d-59c7800371fd
timedObjectId=asyncMarketEAR.asyncMarketEJB.AuctionMgmtEJB auto-timer?:true persistent?:false
timerService=org.jboss.as.ejb3.timerservice.TimerServiceImpl@d72fc25
initialExpiration=null intervalDuration(in milli sec)=0
nextExpiration=Tue Dec 04 23:09:00 EST 2018 timerState=IN_TIMEOUT info=null]
23:08:50 [AuctionMgmtEJB] (EJB default-9) checking auctions
23:08:50 [AuctionMgmtEJB] (EJB default-9) processed 0 active items
Client builds Calendar using ScheduleExpression
@EJB(beanInterface=AuctionMgmtLocal.class)
private AuctionMgmt auctionMgmt;
...
ScheduleExpression schedule = new ScheduleExpression();
schedule.second("*/10");
schedule.minute("*");
schedule.hour("*");
schedule.dayOfMonth("*");
schedule.month("*");
schedule.year("*");
auctionMgmt.initTimers(schedule);
Server-side EJB method to initialize EJB Timer
public void initTimers(ScheduleExpression schedule) {
cancelTimers();
logger.debug("initializing timers, schedule={}", schedule);
timerService.createCalendarTimer(schedule, new TimerConfig("checkAuctionTimer", false));
}
Initialize Timers Cancels all other Timers - including declarative Timers
public void cancelTimers() {
logger.debug("canceling timers");
for (Timer timer : (Collection<Timer>)timerService.getTimers()) {
timer.cancel();
}
}
Server-side EJB method called back
@Timeout
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void execute(Timer timer) {
logger.info("timer fired: {}", timer);
try {
checkAuction();
}
catch (Exception ex) {
logger.error("error checking auction", ex);
}
}
Server-side output
23:20:00 [AuctionMgmtEJB] (EJB default-8) **** AuctionMgmtEJB init() ***
23:20:00 [AuctionMgmtEJB] (EJB default-8) timerService=org.jboss.as.ejb3.timerservice.TimerServiceImpl@3ae3cbd8
23:20:00 [AuctionMgmtEJB] (EJB default-8) checkAuctionInterval=10000
23:20:00 [AuctionMgmtEJB] (EJB default-8) timer fired: [id=a68ba008-1020-49a6-a755-68bbaac4fae3
timedObjectId=asyncMarketEAR.asyncMarketEJB.AuctionMgmtEJB auto-timer?:false persistent?:false
timerService=org.jboss.as.ejb3.timerservice.TimerServiceImpl@3ae3cbd8
initialExpiration=null intervalDuration(in milli sec)=0
nextExpiration=Tue Dec 04 23:20:10 EST 2018 timerState=IN_TIMEOUT info=checkAuctionTimer]
23:20:00 [AuctionMgmtEJB] (EJB default-8) checking auctions
23:20:00 [AuctionMgmtEJB] (EJB default-8) processed 0 active items
23:20:10 [AuctionMgmtEJB] (EJB default-6) **** AuctionMgmtEJB init() ***
23:20:10 [AuctionMgmtEJB] (EJB default-6) timerService=org.jboss.as.ejb3.timerservice.TimerServiceImpl@3ae3cbd8
23:20:10 [AuctionMgmtEJB] (EJB default-6) checkAuctionInterval=10000
23:20:10 [AuctionMgmtEJB] (EJB default-6) timer fired: [id=a68ba008-1020-49a6-a755-68bbaac4fae3
timedObjectId=asyncMarketEAR.asyncMarketEJB.AuctionMgmtEJB auto-timer?:false persistent?:false
timerService=org.jboss.as.ejb3.timerservice.TimerServiceImpl@3ae3cbd8
initialExpiration=null intervalDuration(in milli sec)=0
nextExpiration=Tue Dec 04 23:20:20 EST 2018 timerState=IN_TIMEOUT info=checkAuctionTimer]
23:20:10 [AuctionMgmtEJB] (EJB default-6) checking auctions
23:20:10 [AuctionMgmtEJB] (EJB default-6) processed 0 active items
23:20:20 [AuctionMgmtEJB] (EJB default-7) **** AuctionMgmtEJB init() ***
23:20:20 [AuctionMgmtEJB] (EJB default-7) timerService=org.jboss.as.ejb3.timerservice.TimerServiceImpl@3ae3cbd8
23:20:20 [AuctionMgmtEJB] (EJB default-7) checkAuctionInterval=10000
23:20:20 [AuctionMgmtEJB] (EJB default-7) timer fired: [id=a68ba008-1020-49a6-a755-68bbaac4fae3
timedObjectId=asyncMarketEAR.asyncMarketEJB.AuctionMgmtEJB auto-timer?:false persistent?:false
timerService=org.jboss.as.ejb3.timerservice.TimerServiceImpl@3ae3cbd8 initialExpiration=null
intervalDuration(in milli sec)=0 nextExpiration=Tue Dec 04 23:20:30 EST 2018 timerState=IN_TIMEOUT info=checkAuctionTimer]
23:20:20 [AuctionMgmtEJB] (EJB default-7) checking auctions
23:20:20 [AuctionMgmtEJB] (EJB default-7) processed 0 active items
"Using the Timer Service", Oracle, https://docs.oracle.com/javaee/7/tutorial/ejb-basicexamples004.htm, 2014 Oracle