Kurs Springa – część 2 – adnotacje JPA i baza danych.

Wiemy już jak rozpocząć projekt za pomocą Spring Initializr, teraz zajmiemy się bazą danych i standardem JPA. JPA (Java Persistence API) to tak zwany standard ORM, czyli mapowania obiektowo – relacyjnego (object – relational mapping). Mapowanie to jest sposobem odwzorowania struktury obiektowego systemu na relacyjną bazę danych. Polega to na tym, że w relacyjnej bazie danych mamy tabele, które są ze sobą powiązane kluczami (połączenia takie nazywane są relacjami), natomiast w systemie obiektowym – są to obiekty ze swoimi polami.

Jedna z najprostszych do skonfigurowania relacyjnych baz danych to H2, uruchamiana w pamięci komputera. Ma to swoje dobre i złe strony, jednakże przewagą nad innymi jest właśnie prostota konfiguracji. Baza uruchamia się w pamięci wraz ze startem serwera aplikacyjnego, jest łatwo dostępna przez konsolę, oraz zapewnia to wszystko, co znamy z innych baz typu SQL. W celu dołączenia bazy H2 do swojego projektu, należy w Spring Initializr zaznaczyć H2 w sekcji SQL w opcjach dodatkowych. Przejdźmy do kodowania.

Konwencja tworzenia oprogramowania w Javie EE (jak również w Javie ogólnie) zaleca wykorzystanie paczek (java package) do zorganizowania klas. Jeśli chodzi o encje (tj. obiekty będące odwzorowaniem bazodanowych tabel) najczęściej wykorzystywane są paczki o nazwach dbo (database object), model lub połączenie tych dwóch – db.model (w tym ostatnim przypadku często klasy dostępu do danych ukrywają się w paczce db.dao)

W tym przypadku encje zapiszemy w paczkach model, natomiast dao – w repositories (jest to dość powszechne w Springu, gdzie DAO określa się mianem repozytoriów danych).

Tworzenie encji

Mamy już nasz projekt skonfigurowany, należy dodać do niego trochę “mięsa”. W tym celu utworzymy dwa POJO w paczce it.gniado.spring5webapp.model – Book oraz Author. W klasie książki umieścimy takie pola jak title oraz isbn (oba typu String), oraz authors, będący zbiorem obiektów typu Author (Set<Author>). W klasie Author natomiast – dodamy jedynie firstName oraz lastName (również oba pola typu String). Dodawszy konstruktory, gettersy, settery oraz hashCode, equals i toString, nasze klasy powinny przedstawiać się następująco:

package it.gniado.spring5webapp.model;

import java.util.HashSet;
import java.util.Set;

public class Book {

    private String title;
    private String isbn;
    private Set<Author> authors = new HashSet<>();

    public Book() {
    }

    public Book(String title, String isbn) {
        this.title = title;
        this.isbn = isbn;
    }

    public Book(String title, String isbn, Set<Author> authors) {
        this.title = title;
        this.isbn = isbn;
        this.authors = authors;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public Set<Author> getAuthors() {
        return authors;
    }

    public void setAuthors(Set<Author> authors) {
        this.authors = authors;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (!(o instanceof Book))
            return false;

        Book book = (Book) o;

        return id != null ? id.equals(book.id) : book.id == null;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("Book{");
        sb.append("id=").append(id);
        sb.append(", title='").append(title).append('\'');
        sb.append(", isbn='").append(isbn).append('\'');
        sb.append(", authors=").append(authors);
        sb.append('}');
        return sb.toString();
    }

}
package it.gniado.spring5webapp.model;

import java.util.HashSet;
import java.util.Set;

public class Author {

    private Long id;
    private String firstName;
    private String lastName;
    private Set<Book> books = new HashSet<>();

    public Author() {
    }

    public Author(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public Author(String firstName, String lastName, Set<Book> books) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.books = books;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Set<Book> getBooks() {
        return books;
    }

    public void setBooks(Set<Book> books) {
        this.books = books;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (!(o instanceof Author))
            return false;

        Author author = (Author) o;

        return id != null ? id.equals(author.id) : author.id == null;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("Author{");
        sb.append("id=").append(id);
        sb.append(", firstName='").append(firstName).append('\'');
        sb.append(", lastName='").append(lastName).append('\'');
        sb.append(", books=").append(books);
        sb.append('}');
        return sb.toString();
    }

}

W tym momencie są to zwykłe obiekty POJO (Plain Old Java Object – zwykły, stary obiekt Javy).

Adnotacje JPA

Aby przekształcić je w pełnoprawne encje (rozumiane przez JPA jako elementy odwzorowujące stan bazy) nalezy skorzystać z kilku adnotacji:

  • @Entity
    informuje, że klasa istotnie jest encją. Nadawanie encji nazwy (jako parametr dodany w nawiasie) nie jest konieczne. Pozwoli to wykorzystywanie nadanej nazwy jako alias w zapytaniach JPQL, ale pomijając te działanie – skorzystamy po prostu z nazwy klasy. Encyjność wymaga tego, aby wszystkie pola były prywatne i miały publiczne gettery/settery. Tabela utworzona z takiej encji będzie się nazywała tak jak klasa, a jej kolumny – jak pola.
  • @Id
    adnotacja Entity wymaga istnienia unikalnego identyfikatora, jak w tabelach. Rozwiązuje się to poprzez nadanie jednemu z pól właśnie adnotacji @Id. Najczęściej ta adnotacja nadawana jest polu typu Long.
  • @GeneratedValue
    dodatkowa adnotacja dla pola @Id. Oznacza ona, że wartość pola będzie generowana automatycznie podczas zapisu encji do bazy.

Rodzaje relacji

Każda relacja w JPA może być jedno lub dwukierunkowa. Jednokierunkowa oznacza, że tylko w jednej z klas będzie odwołanie do drugiej z nich, dwukierunkowa natomiast – że odwołanie do drugiej strony relacji będzie po każdej ze stron (w naszym wypadku w klasie Book będzie odwołanie do Author, a w klasie Author – do Book).

  • @OneToOne
    łączy encje w sposób jeden do jednego. Wydaje się być niepotrzebne, czy nieefektywne z poziomu bazy danych, jednakże upraszcza kod pozbywając nas klas z dziesiątkami pól. Należy pamiętać, że w przypadku jeden do jednego, żadna ze stron relacji nie może być kolekcją.
  • @OneToMany / @ManyToOne
    relacja jeden do wielu / wiele do jednego. Pozwala na połączenie np. jednej osoby z wieloma adresami (np. adres zameldowania, zamieszkania etc), lub jednego adresu z wieloma osobami (np. dom rodzinny – pod jednym adresem mieszka kilka osób). Wymaga, aby jedna (i tylko jedna) ze stron była kolekcją.
  • @ManyToMany
    To samo co wyżej, jednak w zestawie wiele do wielu. Może być wykorzystane np. w przypadku adresów jak wyżej (jedna osoba ma wiele adresów, oraz pod jednym adresem mieszka kilka osób). Własnie z tej adnotacji skorzystamy łącząc ze sobą książkę i autora (książkę może napisać kilku autorów, szczególnie książki naukowe, natomiast jeden autor może napisać wiele książek).

Klasy Book i Author po przystosowaniu ich do standardu JPA:

package it.gniado.spring5webapp.model;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String title;
    private String isbn;

    @ManyToMany
    private Set<Author> authors = new HashSet<>();

    public Book() {
    }

    public Book(String title, String isbn) {
        this.title = title;
        this.isbn = isbn;
    }

    public Book(String title, String isbn, Set<Author> authors) {
        this.title = title;
        this.isbn = isbn;
        this.authors = authors;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public Set<Author> getAuthors() {
        return authors;
    }

    public void setAuthors(Set<Author> authors) {
        this.authors = authors;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (!(o instanceof Book))
            return false;

        Book book = (Book) o;

        return id != null ? id.equals(book.id) : book.id == null;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("Book{");
        sb.append("id=").append(id);
        sb.append(", title='").append(title).append('\'');
        sb.append(", isbn='").append(isbn).append('\'');
        sb.append(", authors=").append(authors);
        sb.append('}');
        return sb.toString();
    }

}
package it.gniado.spring5webapp.model;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

@Entity
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;

    @ManyToMany(mappedBy = "authors")
    private Set<Book> books = new HashSet<>();

    public Author() {
    }

    public Author(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public Author(String firstName, String lastName, Set<Book> books) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.books = books;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Set<Book> getBooks() {
        return books;
    }

    public void setBooks(Set<Book> books) {
        this.books = books;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (!(o instanceof Author))
            return false;

        Author author = (Author) o;

        return id != null ? id.equals(author.id) : author.id == null;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("Author{");
        sb.append("id=").append(id);
        sb.append(", firstName='").append(firstName).append('\'');
        sb.append(", lastName='").append(lastName).append('\'');
        sb.append(", books=").append(books);
        sb.append('}');
        return sb.toString();
    }

}

Konsola bazy danych H2

Po wystartowaniu serwera, możemy spróbować połączyć się z konsolą bazy danych. Aby tego dokonać, należy pamiętać o dodaniu odpowiedniego wpisu do pliku application.properties (powinien znajdować się w katalogu resources, jeśli go nie ma – należy go utworzyć). W pliku tym na ten moment potrzebujemy tylko jednej linii:

spring.h2.console.enabled=true

Teraz, po wystartowaniu serwera, możemy połączyć się z konsolą wpisując w przeglądarkę adres http://localhost:8080/h2-console . Połączywszy się z bazą zobaczymy, że automatycznie zostały stworzone tabele w oparciu o nasze encje, co więcej – dla relacji @ManyToMany zostały utworzone tabele złączeniowe. Jedna rzecz jedynie może być niepokojąca – utworzone zostały dwie tabele – dla relacji Books -> Authors, oraz dla relacji Authors -> Books. Dużo lepiej byłoby, gdybyśmy mieli jedną taką tabelę.

Parametry połączenia bazy H2 uruchomionej w pamięci

.

Tabele, które się utworzyły

W tym celu możemy skorzystać z dodatkowej adnotacji JPA – @JoinTable, a istniejącą relację uzupełnić o argument mappedBy – wskazujący który element w drugiej klasie jest odpowiedzialny za związanie encji. Finalny kształt klas Book oraz Author:

package it.gniado.spring5webapp.model;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String title;
    private String isbn;

    @ManyToMany
    @JoinTable(name = "author_book", joinColumns = @JoinColumn(name = "book_id"), inverseJoinColumns = @JoinColumn(name = "author_id"))
    private Set<Author> authors = new HashSet<>();

    public Book() {
    }

    public Book(String title, String isbn, Publisher publisher) {
        this.title = title;
        this.isbn = isbn;
        this.publisher = publisher;
    }

    public Book(String title, String isbn, Publisher publisher, Set<Author> authors) {
        this.title = title;
        this.isbn = isbn;
        this.publisher = publisher;
        this.authors = authors;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public Set<Author> getAuthors() {
        return authors;
    }

    public void setAuthors(Set<Author> authors) {
        this.authors = authors;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (!(o instanceof Book))
            return false;

        Book book = (Book) o;

        return id != null ? id.equals(book.id) : book.id == null;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("Book{");
        sb.append("id=").append(id);
        sb.append(", title='").append(title).append('\'');
        sb.append(", isbn='").append(isbn).append('\'');
        sb.append(", authors=").append(authors);
        sb.append('}');
        return sb.toString();
    }

}
package it.gniado.spring5webapp.model;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

@Entity
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;

    @ManyToMany(mappedBy = "authors")
    private Set<Book> books = new HashSet<>();

    public Author() {
    }

    public Author(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public Author(String firstName, String lastName, Set<Book> books) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.books = books;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Set<Book> getBooks() {
        return books;
    }

    public void setBooks(Set<Book> books) {
        this.books = books;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (!(o instanceof Author))
            return false;

        Author author = (Author) o;

        return id != null ? id.equals(author.id) : author.id == null;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("Author{");
        sb.append("id=").append(id);
        sb.append(", firstName='").append(firstName).append('\'');
        sb.append(", lastName='").append(lastName).append('\'');
        sb.append(", books=").append(books);
        sb.append('}');
        return sb.toString();
    }

}

Oraz ich reprezentacja w bazie danych:

Teraz istnieje już tylko jedna tabela – AUTHOR_BOOK

Jak widać w klasie Author do pola z relacją (books) dodaliśmy w adnotacji relacji @ManyToMany informację o mapowaniu (mappedBy = “authors”). Oznacza to, że w klasie, która jest po drugiej stronie relacji (czyli Book) pole authors wskazuje na tę właśnie encję.

W klasie Book natomiast mamy inną adnotację – @JoinTable. Jest to adnotacja informująca JPA, że należy stworzyć tabelę złączeniową, wraz z jej nazwą, oraz kolumnami łączącymi encje.

@JoinTable(name = "author_book", joinColumns = @JoinColumn(name = "book_id"), inverseJoinColumns = @JoinColumn(name = "author_id"))
  • @JoinTable
    adnotacja ta informuje o istnieniu tabeli złączeniowej. Zawiera ona informacje o nazwie tabeli złączeniowej, oraz odpowiednich kolumnach

    • name – definiuje nazwę tabeli złączeniowej
    • joinColumns – definiuje relację między tabelą złączeniową, a encją będącą właścicielem relacji (w tym wypadku Book)
    • inverseJoinColumns – definiuje relację między tabelą złączeniową, a encją nie będącą właścicielem relacji (w tym wypadku Author)
  • @JoinColumn
    wykorzystywana jest do określania jak ma wyglądać kolumna w bazie danych odpowiedzialna za relację. W tym wypadku podawana po to, by określić nazwę kolumny w tabeli złączeniowej

    • name – jak wyżej – korzysta się z tego atrybutu aby nadać nazwę, tym razem kolumnie

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *