Spring Data Jpa¶
in the application.properties
file (which is discussed here) add the line server.port = 8081
.
NOTE: If you do not have access to
IntelliJ Ultimate
, you can also run the application using Maven and the command line:mvn spring-boot: run
.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Auto configuration¶
spring-boot-starter-data-jpa
, like other starters, it simplifies the work of programmers by freeing them from having to configure certain things. This starter relieves us from the need to [configuration] (../jdbc_hibernate/hibernate/ basic_configuration.md) Hibernata
and allows you to easily configure theDataSource
(e.g. in the [application.properties] file (application_configuration.md # file- applicationproperties)).
Database types¶
Relational databases can be divided into two main groups, i.e. those that:
- store data in memory (e.g. H2, HSQLDB, Apache Derby)
- store data in files (MySQL, MariaDB, OracleDB, MSSQL)
spring-boot-starter-data-jpa
will configureDataSource
differently depending on what database drivers are on classpath:
- if there is a in-memory base depending on the dependencies - connection to it will be configured automatically
- if a database driver is found in dependencies, e.g. MySQL and H2 (in-memory), an attempt will be made to connect to the database "not in memory" (MySQL in this example)
Base H2¶
The H2 database is a very convenient database that is worth using during the software development process (eg in the [profile] (profile_i_scopy.md # profile) development). We can choose it during [bootstrapping] (spring_boot_basics.md#creating-projects) of the project. Default user is sa
, password is empty, dialect used isdialect: org.hibernate.dialect.H2Dialect
and driver is org.h2.Driver
. By default, the database name is generated, and the address is listed in the logs during startup, e.g .:
2020-08-28 21:48:21.508 INFO 17728 --- [main] o.s.b.a.h2.H2ConsoleAutoConfiguration : H2 console available at '/h2'. Database available at 'jdbc:h2:mem:679bb4b4-7115-4e55-8eae-f423e8f97900'
The H2 database allows you to view its content using a browser. By default, however, this console is turned off. We can enable it and set the path to connect it with:
spring.h2.console.path=/h2
spring.h2.console.enabled=true
DataSource configuration¶
Automatically selected values representing [DataSource] (../ jdbc_hibernate/JDBC/connections.md) can be changed using the following [configuration] properties (application_configuration.md # applicationproperties-file):
spring.datasource.username
spring.datasource.url
spring.datasource.password
spring.datasource.driver-class-name
EntityManager¶
Thanks to the use of spring-boot-starter-data-jpa
, we are able to inject the EntityManager
object into any bean. It allows you to perform simple operations (CRUD) and query execution using the HQL language
. Remember that the components representing the database layer should be better marked with the @Repository
annotation instead of the@Component
annotation. The following example shows its use:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "fruits")
public class Fruit {
@Id
@GeneratedValue
private Long id;
private String name;
private Double weight;
public Fruit(final String name, final Double weight) {
this.name = name;
this.weight = weight;
}
}
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
@Repository
public class FruitRepository {
private final EntityManager entityManager;
public FruitRepository(final EntityManager entityManager) {
this.entityManager = entityManager;
}
public Optional<Fruit> getById(final Long id) {
return Optional.ofNullable(entityManager.find(Fruit.class, id));
}
public List<Fruit> findAll() {
return entityManager.createQuery("SELECT f FROM fruits f", Fruit.class).getResultList();
}
public void delete(final Fruit fruit) {
entityManager.remove(fruit);
}
public void deleteById(final Long id) {
entityManager.createQuery("DELETE FROM fruits f WHERE f.id = :id")
.setParameter("id", id)
.executeUpdate();
}
public Fruit createFruit(final Fruit fruit) {
entityManager.persist(fruit);
return fruit;
}
}
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class DbInitializer implements CommandLineRunner {
private final FruitRepository fruitRepository;
public DbInitializer(final FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
@Override
public void run(final String... args) throws Exception {
final Fruit fruitA = new Fruit("Apple", 20.0);
final Fruit fruitB = new Fruit("Banana", 18.0);
fruitRepository.createFruit(fruitA); // (x)
fruitRepository.createFruit(fruitB);
}
}
The class DbInitializer
, defined above, will throw an exception with the following when invoking the line of code marked with (x)
:
No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call
This fact is due to the fact that nowhere in the above code have we defined the transaction
.
@Transactional annotation¶
Manually creating and managing transactions can be cumbersome and the code can become unreadable. Spring, by using aspect programming (which is beyond the scope of this training), eliminates all of these problems. It offers an annotation @Transactional
, which we can place over public methods or a class (which is equivalent to placing it on all public methods in the class). This annotation ensures that the code that uses it runs in the transaction. By default, if a method marked with this annotation is called and a transaction has already been opened, a new transaction will not be started.
NOTE: When importing the
@Transactional
annotation, we can choose between the one included in thejavax.transaction
package ororg.springframework.transaction.annotation
. The framework will create a transaction if you choose any of them, however, the one in theorg.springframework.transaction.annotation
package offers more functionalities (which are beyond the scope of this training).NOTE: The bug in the previous example can be corrected by adding an
@Transactional
annotation over theFruitRepository
class.
JPA repositories¶
The database layer, ie the classes representing the repositories, usually have methods representing the base CRUD
operations, the implementation of which most often looks identical. Therefore, the makers of the spring-boot-starter-data-jpa
added functionality that allows you to implement repositories by defining an interface. Implementation of such interface will be generated, and access to it is provided by the proxy mechanism.
We have the option of extending one of the three interfaces:
CrudRepository
PagingAndSortingRepository
JpaRepository
The basic interface is CrudRepository
which provides access to the methods:
save
saveAll
findById
existsById
findAll
findAllById
count
deleteById
deleteAll
It is worth paying attention to the save
method, which is responsible for both creating and updating a record in the database. It therefore relieves us of the need to use the merge
(update) andpersist
(save) methods available on the EntityManager
object.
The other two interfaces give access to additional methods, e.g .:
findAll(Sort)
findAll(Pageable
saveAndFlush
getOne
deleteInBatch
deleteAllInBatch
Extending one of the interfaces requires us to provide two generics:
- entity identifier type
- entity type
The example shows the use of such an interface:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "fruits")
public class Fruit {
@Id
@GeneratedValue
private Long id;
private String name;
private Double weight;
public Fruit(final String name, final Double weight) {
this.name = name;
this.weight = weight;
}
}
import org.springframework.data.repository.CrudRepository;
public interface FruitRepository extends CrudRepository<Fruit, Long> {
}
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class DbInitializer implements CommandLineRunner {
private final FruitRepository fruitRepository;
public DbInitializer(final FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
@Override
public void run(final String... args) throws Exception {
Fruit apple = fruitRepository.save(new Fruit("apple", 23.1));
Fruit banana = fruitRepository.save(new Fruit("banana", 20.1));
fruitRepository.findAll().forEach(System.out::println); // Fruit(id=1, name=apple, weight=23.1) Fruit(id=2, name=banana, weight=20.1)
System.out.println(fruitRepository.count()); // 2
fruitRepository.deleteAll(List.of(apple, banana));
System.out.println(fruitRepository.count()); // 0
}
}
NOTE: JPA repositories do not need to be marked with any annotation. Their implementation will be in a context that we can inject via interface.
NOTES: Above the interfaces representing the repository we do not need to add annotation
@Transactional
.
Creating queries¶
JPA repositories have unique functionality. They allow you to create queries using the name of the method defined in the interface. Its implementation will be generated. When creating such inquiries, we must remember about certain rules:
- while retrieving data, the method name must begin with the word
find
,read
,query
orget
(find
will be used in the examples). After that keyword, we insert the optional keyword followed by the wordBy
- after the word
By
we give ( optional ) details of the query that can be identified with the part of SQL query, which follows the keyword WHERE.- these parts should point to field names of the entity whose value we are asking for (we start with a capital letter)
- when we want to ask for the value of several fields, we combine the field names, just like in a regular SQL query, with the word
Or
And
- depending on the value of how many fields we ask, the method should have an argument of the appropriate type for each of them (in order)
For example, for the Fruit
entity from the previous example:
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
public interface FruitRepository extends CrudRepository<Fruit, Long> {
Optional<Fruit> findByWeight(Double weight); // SELECT TOP 1 * FROM fruits WHERE weight = ?
Optional<Fruit> findByName(String name); // SELECT TOP 1 * FROM fruits WHERE name = ?
Optional<Fruit> findByWeightAndName(Double weight, String name); // SELECT TOP 1 * fruits WHERE weight = ? AND name = ?
Optional<Fruit> findByNameOrWeight(String name, Double weight); // SELECT TOP 1 * fruits WHERE name = ? OR weight = ?
}
NOTE: If you define a method with an incorrect name, you will be informed about it when starting the application.
Return types¶
When creating queries using method names, we can insert several optional keywords between the word find
andBy
, which also have their SQL equivalents, e.g .:
Distinct
All
TopX
, whereX
is a natural number
Depending on whether the query can return at most one record or many, we should select the returned data type appropriately. Some of the possibilities are:
-
if you want to download at most one object:
Optional
- entity object (
null
in case no record found)
-
when downloading multiple records:
List<TypEncji>
Set<TypEncji>
For example:
import org.springframework.data.repository.CrudRepository;
import java.util.List;
import java.util.Optional;
public interface FruitRepository extends CrudRepository<Fruit, Long> {
Optional<Fruit> findByWeight(Double weight);
Fruit findByName(String name);
List<Fruit> findAllDistinctByName(String name);
List<Fruit> findTop3ByName(String name);
}
Advanced queries¶
In addition to the functionalities described above, JPA repositories also offer many keywords that can be used to filter the downloaded data even more effectively (or use other SQL keywords). Some of them are:
LessThan
GreaterThan
After
Before
StartingWith
EndingWith
Containing
In
NotIn
Like
NotLike
IgnoreCase
OrderBy
We place these words either after the field names or at the end of the query (such as OrderBy
). The rules are very similar to those in SQL.
Below is an example of a repository that defines some more complex queries:
import org.springframework.data.repository.CrudRepository;
import java.util.List;
import java.util.Set;
public interface FruitRepository extends CrudRepository<Fruit, Long> {
List<Fruit> findAllByNameStartingWithAndWeightBetweenOrderByNameDesc(String nameStartingWith, Double minWeight, Double maxWeight);
List<Fruit> findAllByWeightLessThan(Double upperBound);
List<Fruit> findFirstByNameContaining(String nameContaining);
List<Fruit> findAllByNameIn(Set<String> names);
List<Fruit> findAllByNameLike(String nameLike);
List<Fruit> countDistinctByNameEndingWith(String nameEnding);
}
NOTE: IntelliJ Ultimate suggests syntax when creating queries inside JPA repository.
Custom Queries¶
Sometimes there is a need to write a query that is very long or that cannot be created with a method name and we are forced to use the syntax HQL
. For this, we can define a method that has any name, but which is annotated with @Query
. Inside the Query
we put the body of the HQL query.
- if the query has parameters, we put them as method arguments, and give the names inside the
@Query
annotation from the packageorg.springframework.data.jpa.repository
- if the method modifies the database, the method is additionally marked with the annotation
@Modifying
- if we prefer to write the query using native SQL, we can set the value
true
in the fieldnativeQuery
The following example represents such a custom query:
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface FruitRepository extends CrudRepository<Fruit, Long> {
@Query(value = "SELECT f FROM fruits f WHERE f.name = :name", nativeQuery = false)
List<Fruit> selectAllFruitsByProvidedName(@Param("name") String name);
}