jpa

JPA

Java Persistence API


Probleme mit JDBC

  • Connectionverwaltung
  • CRUD-Operationen boilerplate
  • SQL Db-spezifisch
  • Typsicherheit

Architektur

or-mapping


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 //(strategy = GenerationType.AUTO)
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();

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

JpaEntitiyStates
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
  1. updated entity im Context mit übergebener
  2. find(entityClass, entity.pk)
  3. 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;

    @OneToOne

    Each customer has one address

    1 to 1 relationship example

    
    @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


    @OneToMany/@ManyToOne

    n to 1 relationship example


    @Entity
    public class Book {
        @ManyToOne(fetch = FetchType.EAGER)   // default
        @JoinColumn(unique = false)           // default
        private Publisher publisher;
    @Entity
    public class Publisher {
        @OneToMany(
            mappedBy = "publisher",
            fetch = FetchType.LAZY)           //default
        private Collection<Book> books = new HashSet<>();

    @ManyToMany

    n to 1 relationship example

    @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)     //default
        private Collection<Actor> actors = new HashSet<>();
    
        public Collection<Actor> getActors() {
            return actors;
        }
    @Entity
    public class Actor {
        @ManyToMany(
                mappedBy = "actors",
                fetch = FetchType.LAZY)         //default
        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 integrity constraint violation

    @PreRemove

    // Movie still owning
    
    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

    n to 1 relationship example

    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); // Order deleted
    @Entity
    @SoftDelete
    public class Order { }
        
    entityManager.remove(orderWithId42);
    id customer deleted
    41 575 false
    42 1243 true
    43 1243 false

    JPQL


    Eigenschaften


    Ausführung

    var query = entityManager.createQuery("""
        select address from Address address""", 
        Address.class); // sonst Object
    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)
        

    Funktionen


    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


    Vererbung

    uml


    @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