The Java Persistence API offers a powerful standard for persisting POJOs.It includes all the most important features which you would expect from an object-relational mapping tool, but there are still some areas where you might need to use vendor-specific features. In this tutorial, we will show how to use Hibernate-specific features for validating and querying while using the standard API for the rest.
This tutorial assumes you have some basic knowledge of, or programming experience with, the following technologies:
This tutorial is partly based on the Using Hibernate With Java Persistence API tutorial, you might want to go through it first.
Before you begin, you need to install the following software on your computer:
First we will create a library in the IDE for the Hibernate entity manager.
<property name="hibernate.dialect" value="org.hibernate.dialect.DerbyDialect" />
Now we can quickly generate JSF pages for the entity classes with the NetBeans IDE CRUD generation.
At this point we will try to run the application and see whether everything is working as expected.
When creating a new customer you might have noticed that values were not validated before attempting to persist the newly created customer. We could use the validation facilities in JSF for ensuring that only correct values are inserted, but since this tutorial is about Hibernate we will instead demonstrate how to use the Hibernate validation framework. This approach has an additional advantage that the validation rules need to be specified only once even if another type of client is added.
Open Customer.java in the editor and add the following annotations on its member variables:
@Column(name = "STATE") @Length(min=2, max=2, message="not a valid state") private String state; @Column(name = "EMAIL") @Email private String email;For simplicity's sake we will only add validation to state and email properties. The @Length annotation validates that the length of the property will be within the range specified by min and max attributes. We apply it on the state property to make sure that it is exactly 2 characters long. We also specify a value for the message attribute, it will be the shown error message when the validation fails. The @Email annotation in turn validates that the property represents a valid email address, as you might have guessed.
Now that we have our domain object annotated, we need to add handling of validation errors to our controller class. So open CustomerController.java and add the following method there:
/** * Validates the given customer. * @return true if there were no validation errors, false otherwise. */ private boolean validate(Customer customer){ ClassValidator customerValidator = new ClassValidator(Customer.class); // get the invalid values InvalidValue[] msgs = customerValidator.getInvalidValues(customer); if (msgs.length > 0){ for(InvalidValue msg : msgs){ // add an error message for each invalid value, these // messages will be shown to the user addErrorMessage(msg.getMessage()); } return false; } return true; }This method will first create a class validator for our Customer class and then process the validation rules we specified earlier when. We collect the invalid value messages and add each of them as error messages to the FacesContext (this is done by the addErrorMessage method). If there were no validation errors the will return true, false otherwise. Of course, as such this method is not very useful unless we invoke it in the right places. We probably want to validate the values both when a new customer is created and when an existing customer is edited. So let's first modify the create method to check whether there were any validation errors before attempting to persist:
public String create() { if (!validate(customer)){ // return to the input page if there were any validation errors return null; } EntityManager em = getEntityManager(); try { utx.begin(); em.joinTransaction(); em.persist(customer); ...
As you can see, we return null if any errors were found - this means JSF will display the same page again. Make a similar modification to the edit method as well and run the application. Try to create a new customer with an invalid email address and with a 3 characters long state code. This is what you should see:
While the Java Persistence QL is an impressive query language, there are cases when a different kind of API is more suitable. Luckily, in addition to JPQL support, Hibernate features a criteria query API which you can leverage for the cases it is needed and stick to the standard API elsewhere in the application. In the following example we will demonstrate the Query By Example approach using Hibernate's Criteria API.
First we need to create a new page for our new query functionality. Create a new page named Query.jsp in the customer folder and paste the following to it:
<%@page contentType="text/html"%> <%@page pageEncoding="UTF-8"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Query By Example</title> </head> <body> <f:view> <h1>Query By Example</h1> <h:messages errorStyle="color: red" infoStyle="color: green" layout="table"/> <h:form> <h:panelGrid columns="2"> <h:outputText value="Zip:"/> <h:inputText id="zip" value="#{customer.customer.zip}" converter="stringConverter" title="Zip" /> <h:outputText value="Name:"/> <h:inputText id="name" value="#{customer.customer.name}" converter="stringConverter" title="Name" /> <h:outputText value="State:"/> <h:inputText id="state" value="#{customer.customer.state}" converter="stringConverter" title="State" /> </h:panelGrid> <h:commandLink action="#{customer.queryByExample}" value="Search"/> </h:form> <h:form> <a href="/HibernateWithJPA/index.jsp">Back to index</a> <br> <h:dataTable value='#{customer.model}' var='item' border="1" cellpadding="2" cellspacing="0"> <h:column> <f:facet name="header"> <h:outputText value="CustomerId"/> </f:facet> <h:commandLink action="#{customer.detailSetup}" value="#{item.customerId}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Zip"/> </f:facet> <h:outputText value="#{item.zip}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Name"/> </f:facet> <h:outputText value="#{item.name}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Addressline1"/> </f:facet> <h:outputText value="#{item.addressline1}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Addressline2"/> </f:facet> <h:outputText value="#{item.addressline2}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="City"/> </f:facet> <h:outputText value="#{item.city}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="State"/> </f:facet> <h:outputText value="#{item.state}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Phone"/> </f:facet> <h:outputText value="#{item.phone}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Fax"/> </f:facet> <h:outputText value="#{item.fax}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Email"/> </f:facet> <h:outputText value="#{item.email}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="CreditLimit"/> </f:facet> <h:outputText value="#{item.creditLimit}"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="DiscountCode"/> </f:facet> <h:outputText value="#{item.discountCode}"/> </h:column> <h:column> <h:commandLink value="Destroy" action="#{customer.destroy}"> <f:param name="customerId" value="#{item.customerId}"/> </h:commandLink> <h:outputText value=" "/> <h:commandLink value="Edit" action="#{customer.editSetup}"> <f:param name="customerId" value="#{item.customerId}"/> </h:commandLink> </h:column> </h:dataTable> </h:form> </f:view> </body> </html>There are a couple of things to note here. Firstly, we have specified "stringConverter" converter for the input fields. Secondly, the 'search' link will try to execute customer.queryByExample method, which we haven't yet implemented. Thirdly, the data table uses customer.model as its underlying model. We will get back to these in a minute, but before that we still need to create a link to our new page. To keep things simple, we will just add it to the customer/List.jsp page, right after the link to the New Customer page:
<h:commandLink action="#{customer.createSetup}" value="New Customer"/> <br> <h:commandLink action="#{customer.querySetup}" value="Query Customers"/>
Now, this link will cause querySetup method to be invoked in the CustomerController.java, so let's implement it next. To do that, add the following to the CustomerController.java:
public String querySetup(){ this.customer = new Customer(); this.model = null; return "customer_query"; }
And to complete the UI side of things, we still need to add a navigation rule to faces-config.xml:
<navigation-rule> <navigation-case> <from-outcome>customer_query</from-outcome> <to-view-id>/customer/Query.jsp</to-view-id> </navigation-case> </navigation-rule>
What is still missing is the actual implementation of the query, and the converter we mentioned earlier. Let's tackle the implementation of the query method first. In the Query.jsp page we defined that the 'Search' link will invoke customer.queryByExample method and that customer.model will be used for the data table. To satisfy the latter, we just need to create a getter for the model in CustomerController.java - press ctrl-space and choose 'create getter getModel for field model'. After that, add the following method:
/** * Queries customers based on the values in our <code>customer</code>. */ public String queryByExample(){ // get the native hibernate session Session session = (Session) getEntityManager().getDelegate(); // create an example from our customer, exclude all zero valued numeric properties Example customerExample = Example.create(customer).excludeZeroes(); // create criteria based on the customer example Criteria criteria = session.createCriteria(Customer.class).add(customerExample); // perform the query and set the result to our model. this.model = new ListDataModel(criteria.list()); return "customer_query"; }
You can see how easily you can access Hibernate's native API - just invoke getDelegate() on the entity manager and cast it to org.hibernate.Session. Once we have the access to Session we can take advantage of the Criteria API. In the above method we create an example criteria based on the customer and execute the query. If you are not familiar with the Criteria API, it is probably worth explaining a bit. By default, all null values will be excluded, which means that the properties on our customer that were null, will not be part of the criteria. In addition, we specify that all zero valued numeric properties will be excluded as well. Here it gets a bit complicated: since JSF by default converts strings without values to empty strings (instead of nulls), we need to create a special converter for dealing with the conversion of strings from the query page. The implementation of the converter is as simple as:
package sample.controller; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.convert.Converter; /** * A converter for string values that does not * convert <code>null</code> values to empty strings. */ public class StringConverter implements Converter{ /** Creates a new instance of StringConverter */ public StringConverter() { } public Object getAsObject(FacesContext context, UIComponent component, String value) { if(value == null || "".equals(value)){ return null; } return value; } public String getAsString(FacesContext context, UIComponent component, Object value) { return value != null ? value.toString() : null; } }
Don't forget to register the converter in faces-config.xml:
<converter> <converter-id>stringConverter</converter-id>> <converter-class>sample.controller.StringConverter</converter-class> </converter>
Finally, we are ready to run the application once again and test the new functionality:
HibernateWithJPA.zip | 46352 bytes | |
jsf-from-entities.JPG | 50178 bytes | |
jsf-from-entities.png | 151411 bytes | |
list-of-customers.JPG | 185621 bytes | |
list-of-customers.png | 314586 bytes | |
not-valid-customer.JPG | 32670 bytes | |
not-valid-customer.png | 84493 bytes | |
query-by-example.JPG | 81683 bytes | |
query-by-example.PNG | 152412 bytes | |
query-by-example.png | 152355 bytes |