The modern development world is full of various specifications designed to make life easier. Knowing the tools, you can choose the right one. Not knowing can make life difficult for yourself. This review will slightly open the veil of secrecy over the concept of JPA - Java Persistence API. I hope, after reading it, you will want to plunge into this mysterious world even deeper.
Introduction
As we know, one of the main tasks of programs is the storage and processing of data. In the good old days, people simply stored data in files. But as soon as simultaneous read and edit access is needed, when there is a load (that is, several accesses are received simultaneously), storing data simply in files becomes a problem. For more information about what problems databases solve and how, I advise you to read the article "
How databases are arranged ". So, we decide to store our data in the database. For a long time, Java has been able to work with databases using the JDBC API (The Java Database Connectivity). You can read more about JDBC here: "
JDBC or how it all starts". But time passed and developers each time faced with the need to write the same type and unnecessary "serving" code (the so-called Boilerplate code) for trivial operations of saving Java objects in the database and vice versa, creating Java objects according to data from the database. And then to solve These problems gave rise to such a thing as ORM.ORM
- Object-Relational Mapping or translated into Russian object-relational mapping.This is a programming technology that connects databases with the concepts of object-oriented programming languages.To simplify, ORM is a connection Java objects and records in the database:
ORM is essentially the concept that a Java object can be represented as data in a database (and vice versa). It was embodied in the form of the JPA specification - Java Persistence API. The specification is already a description of the Java API that expresses this concept. The specification tells what means we must be provided with (i.e. through what interfaces we can work) in order to work according to the ORM concept. And how to use these tools. The specification does not describe the implementation of the means. This makes it possible to use different implementations for the same specification. You can simplify and say that the specification is a description of the API. The text of the JPA specification can be found on the Oracle website: "
JSR 338: JavaTM Persistence API". Therefore, in order to use JPA, we need some implementation with which we will use the technology. JPA implementations are also called JPA Providers. One of the most notable JPA implementations is Hibernate. Therefore, I propose to consider
it .
Create a project
Since JPA is about Java, we need a Java project. We could manually create the directory structure ourselves, add the necessary libraries ourselves. But it is much more convenient and correct to use project build automation systems (that is, in fact, it is just a program that will manage the assembly of projects for us. Create directories, put the necessary libraries in the classpath, etc.). One such system is Gradle. You can read more about Gradle here: "
Getting Started with Gradle ". As we know, the functionality of Gradle (i.e. the actions it can do) is implemented using various Gradle Plugins. Let's use Gradle and the
Gradle Build Init Plugin . Let's execute the command:
gradle init --type java-application
Gradle will make the necessary directory structure for us, create a basic declarative description of the project in the build script
build.gradle
. So, we have an application. We need to think about what we want to describe or model with our application. Let's use some modeling tool, for example:
app.quickdatabasediagrams.com Here it is worth saying that what we have described is our "domain model". A domain is some "subject area". In general, the domain is "possession" in Latin. In the Middle Ages, this was the name of the areas owned by kings or feudal lords. And in French it became the word "domaine", which translates simply as "region". Thus we have described our "domain model" = "subject model". Each element of this model is some "essence", something from real life. In our case, these are entities: Category (
Category
), Topic (
Topic
). Let's create a separate package for the entities, for example, with the name model. And add there Java classes that describe entities. In Java code, such entities are a regular
POJO ,
public class Category {
private Long id;
private String title;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
Let's copy the contents of the class and make the class by analogy
Topic
. He will differ only in that he knows about the category to which he belongs. Therefore, let's add
Topic
a category field and methods for working with it to the class:
private Category category;
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
Now we have a Java application that has its own domain model. It's time to start connecting to the JPA project now.
Adding JPA
So, as we remember, JPA is about the fact that we will save something in the database. Therefore, we need a database. To use a database connection in our project, we need to add a library for connecting to the database as a dependency. As we remember, we used Gradle, which created a build script for us
build.gradle
. In it, we will describe the dependencies that our project needs. Dependencies are those libraries without which our code cannot work. Let's start with a description of the dependence on the connection to the database. We do this in the same way as we would do, working simply with JDBC:
dependencies {
implementation 'com.h2database:h2:1.4.199'
Now we have a DB. We can now add a layer or layer to our application that is responsible for mapping our Java objects to database concepts (from Java to SQL). As we remember, we are going to use an implementation of the JPA specification called Hibernate for this:
dependencies {
implementation 'com.h2database:h2:1.4.199'
implementation 'org.hibernate:hibernate-core:5.4.2.Final'
Now we need to configure JPA. If we read the specification and section "8.1 Persistence Unit", then we will know that the Persistence Unit is some kind of union of configurations, metadata and entities. And for JPA to work, you need to describe at least one Persistence Unit in the configuration file, which has the name
persistence.xml
. Its location is described in the specification chapter "8.2 Persistence Unit Packaging". According to this section, if we have a Java SE environment, then we must put it in the root of the META-INF directory.
The content is copied from the example given in the JPA specification in the chapter "
8.2.1 persistence.xml file
":
<persistence>
<persistence-unit name="CodeGym">
<description>Persistence Unit For test</description>
<class>hibernate.model.Category</class>
<class>hibernate.model.Topic</class>
</persistence-unit>
</persistence>
But this is not enough. We need to tell who our JPA Provider is, i.e. one who implements the JPA specification:
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
And now let's add settings (
properties
). Some of them (beginning with
javax.persistence
) are standard JPA configurations and are described in the JPA specification in section "8.2.1.9 properties". Some of the configurations are provider-specific (in our case, they affect Hibernate as a Jpa Provider. Our settings block will look like this:
<properties>
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
<property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1;MVCC=TRUE" />
<property name="javax.persistence.jdbc.user" value="sa" />
<property name="javax.persistence.jdbc.password" value="" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
Now we have a JPA-compatible config
persistence.xml
, we have a Hibernate JPA provider, we have an H2 database, and we have 2 classes that are our domain model. Let's finally make this work. In the directory
/test/java
, our Gradle kindly generated a template for Unit tests and called it AppTest. Let's use it. As the "7.1 Persistence Contexts" chapter of the JPA specification says, entities in the JPA world live in a space called a "Persistence Context" (or Persistence Context). But we do not work directly with Persistence Context. For this we use
Entity Manager
or "entity manager". It is he who knows about the context and about what entities live there. We interact with
Entity Manager
'om. Then it remains only to understand
Entity Manager
? According to chapter "7.2.2 Obtaining an Application-managed Entity Manager" of the JPA specification, we should use
EntityManagerFactory
. Therefore, we arm ourselves with the JPA specification and take an example from the chapter "7.3.2 Obtaining an Entity Manager Factory in a Java SE Environment" and arrange it in the form of a simple Unit test:
@Test
public void shouldStartHibernate() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory( "CodeGym" );
EntityManager entityManager = emf.createEntityManager();
}
Already this test will show "Unrecognized JPA persistence.xml XSD version" error. The reason is that
persistence.xml
you need to correctly specify the schema used, as stated in the JPA specification in section "8.3 persistence.xml Schema":
<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_2.xsd"
version="2.2">
Also, the order of the elements is important. Therefore,
provider
must be specified before class enumeration. After that, the test will run successfully. We have done the JPA direct connection. Before we move on, let's think about the rest of the tests. Each of our tests will require
EntityManager
. Let's make sure that each test has its own
EntityManager
at the start of execution. In addition, we want the database to be new each time. Due to the fact that we use
inmemory
the variant, it is enough to close the
EntityManagerFactory
. Creation
Factory
is an expensive operation. But for tests - it's justified. JUnit allows you to specify methods that will be executed before (Before) and after (After) the execution of each test:
public class AppTest {
private EntityManager em;
@Before
public void init() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory( "CodeGym" );
em = emf.createEntityManager();
}
@After
public void close() {
em.getEntityManagerFactory().close();
em.close();
}
Now, before executing any test, a new one will be created
EntityManagerFactory
, which will entail the creation of a new database, because.
hibernate.hbm2ddl.auto
matters
create
. And from the new factory we will get a new one
EntityManager
.
Entities
As we remember, we created earlier classes that describe our domain model. We have already said that these are our "essences". This is the Entity that we will manage with
EntityManager
. Let's write a simple test to save the category entity:
@Test
public void shouldPersistCategory() {
Category cat = new Category();
cat.setTitle("new category");
em.persist(cat);
}
But this test will not work right away, because we will get various errors that will help us understand what entities are:
-
Unknown entity: hibernate.model.Category
Why doesn't Hibernate understand what Category
it is entity
? The thing is that entities must be described according to the JPA standard.
Entity classes must be annotated with the annotation @Entity
as stated in chapter "2.1 The Entity Class" of the JPA specification.
-
No identifier specified for entity: hibernate.model.Category
Entities must have a unique ID that can be used to distinguish one record from another.
According to the chapter "2.4 Primary Keys and Entity Identity" of the JPA specification "Every entity must have a primary key", i.e. each entity must have a "primary key". Such a primary key must be specified with an annotation@Id
-
ids for this class must be manually assigned before calling save()
The ID has to come from somewhere. It can be specified manually, or it can be obtained automatically.
Therefore, as mentioned in the chapters "11.2.3.3 GeneratedValue" and "11.1.20 GeneratedValue Annotation", we can specify the annotation @GeneratedValue
.
Thus, in order for the category class to become an entity, we must make the following changes:
@Entity
public class Category {
@Id
@GeneratedValue
private Long id;
In addition, the annotation
@Id
specifies which
Access Type
. You can read more about the access type in the JPA specification, in section "2.3 Access Type". If very briefly, then because. we have indicated
@Id
above the field (
field
), then the access type will default
field-based
to , not
property-based
. Therefore, the JPA provider will read and store values directly from the fields. If we placed
@Id
above the getter, then
property-based
access would be used, i.e. via getter and setter. When executing the test, we see, among other things, which requests are sent to the database (thanks to the option
hibernate.show_sql
). But when saving, we do not see any
insert
's. It turns out that we actually did not save anything? JPA allows you to synchronize the persistence context and the database using the method
flush
:
entityManager.flush();
But if we execute it now, we will get an error:
no transaction is in progress . And here comes the time to learn about how JPA uses transactions.
JPA Transactions
As we remember, JPA is based on the concept of Persistence Context. This is the place where entities live. And we manage entities through
EntityManager
. When we execute a command
persist
, we place the entity in the context. More precisely, we say
EntityManager
'y that it needs to be done. But this context is just some storage area. It is even sometimes referred to as a "first level cache". But it needs to be connected to the database. The command
flush
that we previously failed with an error synchronizes data from the persistence context with the database. But this requires a transport, and that transport is a transaction. Transactions in JPA are described in the specification section "7.5 Controlling Transactions". There is a special API for using transactions in JPA:
entityManager.getTransaction().begin();
entityManager.getTransaction().commit();
We need to add transaction management to our code that runs before and after the tests:
@Before
public void init() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory( "CodeGym" );
em = emf.createEntityManager();
em.getTransaction().begin();
}
@After
public void close() {
if (em.getTransaction().isActive()) {
em.getTransaction().commit();
}
em.getEntityManagerFactory().close();
em.close();
}
After adding, we will see an expression in the SQL language in the insert log, which did not exist before:
The changes accumulated in
EntityManager
were committed (confirmed and saved) to the database with the help of a transaction. Let's try to find our essence now. Let's create a test to search for an entity by its ID:
@Test
public void shouldFindCategory() {
Category cat = new Category();
cat.setTitle("test");
em.persist(cat);
Category result = em.find(Category.class, 1L);
assertNotNull(result);
}
In this case, we will get the entity we saved earlier, but we will not see SELECT queries in the log. And all according to what we say: "Entity manager, please find me an entity Category with ID=1". And the entity manager first looks in its own context (it uses a kind of cache), and only if it doesn’t find it, it goes to look in the database. It is worth changing the ID to 2 (there is no such thing, we only saved 1 instance), as we will see that
SELECT
the request appears. Because no entities were found in the context and
EntityManager
it is trying to find the database entity. There are different commands that we can use to control the state of the entity in the context. The transition of an entity from one state to another is called the life cycle of an entity -
lifecycle
.
Entity Lifecycle
The life cycle of entities is described in the JPA specification in chapter "3.2 Entity Instance's Life Cycle". Because entities live in a context and are controlled by
EntityManager
, then they say that entities are managed, i.e. managed. Let's look at the life stages of an entity:
Category cat = new Category();
cat.setTitle("new category");
entityManager.persist(cat);
entityManager.getTransaction().begin();
entityManager.getTransaction().commit();
entityManager.detach(cat);
Category managed = entityManager.merge(cat);
entityManager.remove(managed);
And here's the schematic for proof:
Mapping
In JPA, we can describe the relationship of entities between each other. Recall that we already dealt with entity relationships between each other when we dealt with our domain model. Then we used the resource
quickdatabasediagrams.com :
Establishing relationships between entities is called mapping or association (Association Mappings). The kinds of associations that can be established using JPA are listed below:
Let's look at an entity
Topic
that describes a topic. What can we say about the relationship
Topic
to
Category
? Many
Topic
will belong to the same category. Therefore, we need an association
ManyToOne
. Let's express this relationship in JPA:
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
To remember which annotations to put, you can remember that the last part is responsible for the field over which the annotation is indicated.
ToOne
- specific instance.
ToMany
- collections. Now we have a one-way connection. Let's make it a two-way link. Let's add to
Category
the knowledge about all
Topic
that fall into this category. It must end with
ToMany
, because we have a list
Topic
. That is, the relation "To many" topics. The question remains -
OneToMany
either
ManyToMany
:
On the same topic, a good answer can be found here: "
Explain ORM oneToMany, manyToMany relation like I'm five ". If a category has a connection with
ToMany
topics, then each of these topics can have only one category, then it will be
One
, otherwise
Many
. So the
Category
list of all topics will look like this:
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "topic_id")
private Set<Topic> topics = new HashSet<>();
And let's not forget to actually
Category
describe a getter to get a list of all topics:
public Set<Topic> getTopics() {
return this.topics;
}
Bidirectional relationships are a very tricky thing to track automatically. Therefore, JPA shifts this responsibility to the developer. For us, this means that when we establish
Topic
a relationship with an entity
Category
, we must ensure the consistency of the data ourselves. This is done simply:
public void setCategory(Category category) {
category.getTopics().add(this);
this.category = category;
}
Let's write a simple test to check:
@Test
public void shouldPersistCategoryAndTopics() {
Category cat = new Category();
cat.setTitle("test");
Topic topic = new Topic();
topic.setTitle("topic");
topic.setCategory(cat);
em.persist(cat);
}
Mapping is a whole separate topic. Within the framework of this review, it should be understood by what means this is achieved. You can read more about mapping here:
JPQL
JPA introduces an interesting tool - queries in the Java Persistence Query Language. This language is similar to SQL but uses the Java object model rather than SQL tables. Consider an example:
@Test
public void shouldPerformQuery() {
Category cat = new Category();
cat.setTitle("query");
em.persist(cat);
Query query = em.createQuery("SELECT c from Category c WHERE c.title = 'query'");
assertNotNull(query.getSingleResult());
}
As we can see, in the query we used an indication of the entity
Category
, and not the table. And also on the field of this entity
title
. JPQL provides many useful features and claims to be a separate article. More details can be found in the review:
Criteria API
And finally, I would like to touch on the Criteria API. JPA introduces a dynamic query building tool. An example of using the Criteria API:
@Test
public void shouldFindWithCriteriaAPI() {
Category cat = new Category();
em.persist(cat);
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Category> query = cb.createQuery(Category.class);
Root<Category> c = query.from(Category.class);
query.select(c);
List<Category> resultList = em.createQuery(query).getResultList();
assertEquals(1, resultList.size());
}
This example is equivalent to executing the query "
SELECT c FROM Category c
".
The Criteria API is a powerful tool. You can read more about it here:
Conclusion
As we can see, JPA provides a huge number of features and tools. Each of them requires experience and knowledge. Even as part of the JPA review, it turned out not to mention everything, not to mention a detailed dive. But I hope that after reading it, it became clearer what ORM and JPA are in general, how it works and what can be done with it. Well, for a snack I offer various materials:
#Viacheslav
GO TO FULL VERSION