JPA
Java Persistence API
Probleme mit JDBC
CRUD-Operationen boilerplate
Architektur
Persistence Unit
resources/META-INF/persistence.xml
<persistence-unit name ="at.htlstp.jpa.library" >
<class > domain.Entity</class >
<properties >
<property name ="jakarta.persistence.jdbc.driver"
value ="org.h2.Driver" />
<property name ="jakarta.persistence.jdbc.url"
value ="jdbc:h2:mem:bibliothek" />
<property name ="hibernate.dialect"
value ="org.hibernate.dialect.H2Dialect" />
<property name ="hibernate.show_sql" value ="true" />
<property name ="hibernate.format_sql" value ="true" />
<property name ="hibernate.hbm2ddl.auto" value ="update" />
</properties >
</persistence-unit >
hibernate.hbm2ddl.auto
none
default
create-only
Erzeugt create-Statements
drop
create
drop, dann create-only
create-drop
drop, dann create-only, finally drop
validate
update
Entities
@Entity
@Table(name = "defaulting_to_classname")
public class Customer implements Serializable {
@Id
@GeneratedValue
private Integer id;
@NotNull
private String name;
@NaturalId
private String email;
}create table defaulting_to_classname (
id integer not null ,
email varchar (255 ),
name varchar (255 ) not null ,
primary key (id))
alter table defaulting_to_classname
add constraint UK_ruvgttp unique (email)
Generation Strategies
@Id
private Long assignedByJava;
@Id
@GeneratedValue
private Integer databaseDefaultStrategy;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long assignedByTableColumn;
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "customer_generator")
@SequenceGenerator(
name = "customer_generator",
sequenceName = "customer_sequence")
private Short assignedByOwnSequence;
Annotations
@Column(
name = "columnName",
unique = true,
length = 10)
private String javaName;
create table Customer (
id integer not null ,
columnName varchar (10 ),
primary key (id))
alter table Customer
add constraint UK_5ru7su37v unique (columnName)
@ElementCollection
private Collection<String> aliases;
create table Customer_aliases (
Customer_id integer not null ,
aliases varchar (255 ))
@Entity
public class Customer {
@Id
private Short id;
@Transient
private String notForDb;
}
create table Customer (
id smallint not null ,
primary key (id))
@Entity
@Check(constraints = """
CASE
WHEN name IS NOT NULL THEN LENGTH(name) > 3
ELSE true
END""")
public class Customer {
jakarta.validation
@NotNull
@Size(max = 100)
@Email
private String email;
@PositiveOrZero
private int age;
@ElementCollection
@Size(min = 1, max = 5)
private Collection<String> aliases;
@Past
private LocalDate dateOfBirth;
@Pattern("^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$")
private String ip;
@AssertTrue
public boolean isBeginningBeforeEnd () {
return beginning.isBefore(end);
}
Dependencies
<dependencies >
<dependency >
<groupId > org.hibernate.validator</groupId >
<artifactId > hibernate-validator</artifactId >
<version > 7.0.4.Final</version >
</dependency >
<dependency >
<groupId > org.glassfish</groupId >
<artifactId > jakarta.el</artifactId >
<version > 4.0.1</version >
</dependency >
</dependencies >
EntityManager
var factory = Persistence
.createEntityManagerFactory("at.htlstp.jpa.library" );
var entityManager = factory.createEntityManager();
Verwaltet Entitäten in einem Persistence Context
Am besten in try-with
Schreibende Db-Operationen nur in Transaktion
Verwendung
var factory = Persistence
.createEntityManagerFactory("at.htlstp.jpa.library" );
try (var entityManager = factory.createEntityManager()) {
var transaction = entityManager.getTransaction();
transaction.begin();
entityManager.persist(customer);
transaction.commit();
} catch (RuntimeException ex) {
if (transaction.isActive())
transaction.rollback();
throw ex;
}
States
transient
Objekt war nie managed
managed /persistent
entityManager.flush() -> DbUpdate
detached
Objekt war managed
Methoden
persist(entity)
find(entityClass, pk)
remove(entity)
merge(entity)
lädt entity in den Context
updated entity im Context mit übergebener
find(entityClass, entity.pk)
persist(entity)
returnt die Entity aus dem Context
equals/hashCode
Beziehungen
Generell
nur der Owner darf in Db schreiben
- bei nicht-owning Seite Attribut `mappedBy`
Lazy Loading
@Basic(fetch = FetchType.EAGER)
private String alwaysLoaded
private String defaultEager;
@Basic(fetch = FetchType.LAZY)
private String loadedIfNeeded;
Performance
lazy detached -> Exception 💥
Fix: select ... join fetch lazyField
@OneToOne
Each customer has one address
@Entity
public class Customer {
@OneToOne(fetch = FetchType.EAGER) // default
@JoinColumn(unique = true, name = "address_id") // default
private Address address;
@Entity
public class Address {
@OneToOne(mappedBy = "address") // der Variablenname im Owner
private Customer resident; // entfernen -> unidirektional
Anwendung
Besser über @Embeddable, außer
Entity auch an anderer Stelle verwendet
Daten aus unterschiedlichen Dbs
@OneToMany/@ManyToOne
Each publisher publishes many books.
Each book has one publisher
@Entity
public class Book {
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(unique = false)
private Publisher publisher;@Entity
public class Publisher {
@OneToMany(
mappedBy = "publisher",
fetch = FetchType.LAZY)
private Collection<Book> books = new HashSet <>();
@ManyToMany
Each movie has many actors in it
Each actor stars in many movies
@Entity
public class Movie {
@ManyToMany
@JoinTable(name = "movie_actor",
joinColumns = @JoinColumn(name = "movie_id"),
inverseJoinColumns = @JoinColumn(name = "actor_id"))
@Entity
public class Movie {
@ManyToMany(fetch = FetchType.LAZY)
private Collection<Actor> actors = new HashSet <>();
public Collection<Actor> getActors () {
return actors;
}
@Entity
public class Actor {
@ManyToMany(
mappedBy = "actors",
fetch = FetchType.LAZY)
private Collection<Movie> movies = new HashSet <>();
public Stream<Movie> getMovies () {
return movies.stream();
}
public void starInMovie (Movie movie) {
movie.getActors().add(this );
movies.add(movie);
}
Owning
public class Movie {
@ManyToMany
private Collection<Actor> actors = new HashSet <>();
...
}
public class Actor {
@ManyToMany(mappedBy = "actors")
private Collection<Movie> movies = new HashSet <>();
...
}
entityManager.merge(movie);
entityManager.remove(movie);
Movie gelöscht, Actor existiert
entityManager.merge(actor);
entityManager.remove(actor);
💥 Referential int egrity constraint violation
@PreRemove
public class Actor {
@ManyToMany(mappedBy = "actors")
private Collection<Movie> movies = new HashSet <>();
@PreRemove
void removeFromAllMovies () {
movies.forEach(movie -> movie.getActors().remove(this ));
}
...
}
entityManager.merge(actor);
entityManager.remove(actor);
Actor gelöscht, kommt in keinem Movie mehr vor
JoinTable mit Extras
Nur mapbar über eigene Entity
@Entity
public class Character {
@ManyToOne
private Actor actor;
@ManyToOne
private Movie movie;
@Column(name = "character")
private String name;
Cascade
@Entity
public class A {
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
private B b;
Attribut bei @XtoY
CascadeType.
{PERSIST, MERGE, REMOVE, ALL}
CascadeType.OP
em.op(a); -> em.op(b);
orphanRemoval
a.setB(otherB);-> em.remove(b);
Deletion
@Entity
public class Order { ... }
entityManager.remove(order);
@Entity
@SoftDelete
public class Order { }
entityManager.remove(orderWithId42);
id
customer
deleted
41
575
false
42
1243
true
43
1243
false
JPQL
Eigenschaften
SQL-ähnliche objektorientierte Abfragesprache
Bezeichner aus Java, keine Tables
Anwendung ausschließlich auf Entities
Ausführung
var query = entityManager.createQuery("""
select address from Address address""" ,
Address.class);
List<Address> addresses = query.getResultList();
query = entityManager.createQuery("""
select address
from Address address
where address.id = 1""" , Address.class);
Address address = query.getSingleResult();
List<Object > addresses = entityManager.createQuery("""
select address
from Address address
where address.city = :city""")
.setParameter("city", "St. Pölten")
.getResultList();
Select
List<Object[] > result = entityManager.createQuery("""
select address.city, address.street, address.zip
from Address address""")
.getResultList();
result.stream()
.map(Arrays::toString)
.forEach(System.out::println);
[St. Pölten, Waldstraße 7 , 3100 ]
[Wien, Antonigasse 13 , 1180 ]
[Wien, Michaelerkuppel 10 , 1010 ]
[Wien, Wienerbergerstraße 1 , 1120 ]
[St. Pölten, Schießstattring 6 , 3100 ]
[Wilhelmsburg, Obere Hauptstraße 42 , 3150 ]
[St. Pölten, Rathausplatz 2 , 3100 ]
[St. Pölten, Rathausplatz 3 , 3100 ]
where
select book.id, book.title
from Book book
where book.title = :title
≈ select new Book(book.id, book.title)
from Book book
where book.title = :title
select distinct customer.lastname
from Customer customer
select book
from Book book
where book.publisher.name like '% HALL'
select book
from Book book
order by book.author, book.title desc
select publisher.name, book
from Publisher publisher, Book book
where publisher = book.publisher
= select publisher.name, book from Publisher publisher
join publisher.books book
select publisher
from Publisher publisher
join publisher.books book
where book.title like :title
select book
from Book book
where book.author is null
or book.id <= 1 and book.publisher.name not like '% HALL'
Collections
Collection<Book> books = em
.createQuery("""
select publisher.books
from Publisher publisher""")
.getResultList();
select book
from Book book
where book.isbn in ('404', '418')
select book
from Book book
where book.isbn in :javaCollection
select book
from Book book
where book.id between 1 and 3
select publisher
from Publisher publisher
where publisher.books is empty
select publisher
from Publisher publisher
where size(publisher.addresses) >= 3
select address
from Address address, Publisher publisher
where address.city = 'St. Pölten'
alle Adressen in StP für alle Publisher
select address
from Address address, Publisher publisher
where address.city = 'St. Pölten'
and address member of publisher.addresses
groupBy
List<Object[]> result = entityManager.createQuery("""
select book.publisher.name, count(book)
from Book book
group by book.publisher.name
having count(book) > 0
order by book.publisher.name""")
.getResultList();
Map<String, Long> map = result.stream()
.collect(toMap(
record -> (String) record[0],
record -> (Long) record[1],
Long::sum, // mergeFunction
LinkedHashMap ::new));
Aggregatfunktionen
select count (*)
from Book -> long
select oldest
from Book oldest
where oldest.publicationDate =
(select min (b.publicationDate)
from Book b)
sum() -> long/double
avg() -> double
Funktionen
upper/lower(String s)
current_date/time/timestamp()
substring(String s, int offset, int length)
trim(String s)
length(String s)
abs(Numeric n)
sqrt(Numeric n)
mod(Numeric a, Numeric b)
Embeddables
@Entity
public class Customer {
@EmbeddedId
private Uid uid;
@Embedded
private Address address;
}@Embeddable
public class Uid implements Serializable {
private String email;
}create table Customer (
email varchar (255 ) not null ,
street varchar (255 ),
zip integer ,
primary key (email))concatenated keys
Wie kommt das SQL zustande?
String, Integer etc. kein Table. Addressist ebenso@Embeddable`, siehe SQL
Vererbung
ein Table mit einer Laufzeittyp-Column
ein Table je Klasse, wobei ein fk zur Basisklasse besteht, wo die vererbten Daten sind
@Entity
@Inheritance
public class Topic {
@Id
@GeneratedValue
private Long id;@Entity
public class Post extends Topic {
private String content;
}@Entity
public class Announcement extends Topic {
@NotNull
private LocalDateTime validUntil;
}
Table Topic(Auszug)
DTYPE
OWNER
VALIDUNTIL
CONTENT
Post
owner
NULL
content
Announcement
owner
2020-04-21 20:39:37.292626
NULL
Post
SCRE
NULL
ok
Post
SCRE
NULL
thx
Announcement
CNN
2020-04-21 20:39:37.292626
NULL