Spring Data – dostęp i zarządzanie danymi

Share on facebook
Share on twitter
Share on linkedin
Spring data

Spring Data

Co to jest Spring Data?

Spring Data jest modułem upraszczającym pracę z warstwą persystencji. Zapewnia on przyjazny i spójny sposób dostępu i zarządzania danymi. Danymi, które mogą być umieszczone gdziekolwiek. Równie dobrze, możesz zaimplementować swoje źródło danych np. dane z dysku.

Biblioteka ta wspiera wiele zródeł danych:

  • JPA (bazy danych SQL),
  • MongoDB,
  • DynamoDB,
  • Apache Cassandra,
  • Redis,
  • Elasticsearch.

Nie są to wszystkie wspierane data source, pełną listę znajdziesz tutaj. Niektóre z nich są rozwijane przez firmy, zaś inne przez społeczność.

W tym artykule skupie się na module Spring Data Jpa, czyli implementacji odpowiedzialnej za integrację z bazami SQL.

Hierarchia Interfejsów Spring Data

Repository

Powyżej wspomniałem, że Spring Data umożliwia pracę z różnymi bazami danych. W tym punkcie trochę rozwinę ten temat.

Podstawową jednostką w Spring Data jest repozytorium. Repozytorium to nic innego jak obiekt, przy użyciu którego wykonujemy operację bezpośrednio na bazie danych.

Poniżej znajduje się interfejs Repository.

@Indexed
public interface Repository<T, ID> {
}

Jak widać, interfejs ten jest pusty, jest on jedynie wspólnym interfejsem dla wszystkich innych interfejsów repozytorium.

Kolejny w hierarchi jest CrudRepository, który ma już coś do zaoferowania. W przeciwieństwie do swojego poprzednika.

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S var1);

    <S extends T> Iterable<S> saveAll(Iterable<S> var1);

    Optional<T> findById(ID var1);

    boolean existsById(ID var1);

    Iterable<T> findAll();

    Iterable<T> findAllById(Iterable<ID> var1);

    long count();

    void deleteById(ID var1);

    void delete(T var1);

    void deleteAll(Iterable<? extends T> var1);

    void deleteAll();
}

Interfejs ten definiuje podstawowe operacje CRUD. Wszystkie te operacje są już zaimplementowane i można ich używać bez konieczności ich pisania.

Kolejnym interfejsem, który znajdziesz w hierarchii, jest interfejs JpaRepository.

@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    List<T> findAll();

    List<T> findAll(Sort var1);

    List<T> findAllById(Iterable<ID> var1);

    <S extends T> List<S> saveAll(Iterable<S> var1);

    void flush();

    <S extends T> S saveAndFlush(S var1);

    void deleteInBatch(Iterable<T> var1);

    void deleteAllInBatch();

    T getOne(ID var1);

    <S extends T> List<S> findAll(Example<S> var1);

    <S extends T> List<S> findAll(Example<S> var1, Sort var2);
}

Rozszerza on interfejs PagingAndSortingRepository, zaś ten rozszerza wcześniej wspomniany CrudRepository.

Dzięki takiemu połączeniu, interfejs jest przyjaźniejszy w użyciu np. metody findAll[…] zwracają typ List zamiast Iterable (co może być lekko irytujące). Dodatkowo dostarcza on możliwość sortowania i stronicowania (ang. paging) pobieranych danych.

Spring Data został zaprojektowany tak, aby można było zaimplementować każde możliwe źródło danych. Równie dobrze, możesz stworzyć swoje repozytorium np. FileRepository, które będzie zapewniało wymianę informacji między aplikacją a dyskiem.

Gdy używasz bazy danych SQL, powinieneś rozszerzyć JpaRepository.

Jednak gdybyś chciał użyć w swoim projekcie np. MongoDB, zamiast JpaRepository wykorzystaj MongoRepository. W tym artykule skupimy się na JpaRepository.

Konfiguracja

Spring-data-jpa

Do swojego pom.xml dodaj zależność spring-boot-starter-data-jpa. Dzięki starterowi Spring Data JPA będzie gotowy do użycia.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Baza danych

W swoim projekcie będę wykorzystywał bazę danych hsqldb in memory.

<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <version>2.4.0</version>
    <scope>runtime</scope>
</dependency>

Warsztat

Na warsztacie pokażę Ci jak skorzystać z modułu Spring Data. Do zrozumienia tego punktu, wymagana jest podstawowa znajomość standardu JPA, a najlepiej również samego Hibernate.

Podczas warsztatów stworzymy:

  • encję odpowiadającą książce,
  • restowy kontroler, przy użyciu którego będzie można wykonać operacje CRUD na wcześniej utworzonej encji.

Tworzenie encji

Encja będzie się nazywała BookEntity, zaś tabela w bazie danych będzie nosiła nazwę books.

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;

@Entity // 1
@NoArgsConstructor
@Getter
@AllArgsConstructor
@Setter
@Table(name = "books") // 2
public class BookEntity {
    @Id // 3
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 4
    private Long id;
    // 5
    private String title;
    private String author;

    public BookEntity(String title, String author) {
        this.title = title;
        this.author = author;
    }
}

Dla osób zaznajomionych ze standardem JPA, powyższy kod nie powinien stanowić problemu. Dla tych nieobytych z JPA, po krótce wytłumaczę powyższy listing.

  1. Oznaczamy klasę jako encję JPA,
  2. Encja będzie przechowywana w tabeli o nazwie books,
  3. Kolumna id będzie oznaczona jako klucz główny tabeli,
  4. Wartości id będą generowane automatycznie,
  5. Tabela będzie miała jeszcze dwie kolumny: title oraz author.

Tworzenie repository

Stworzenie własnego repozytorium, wymaga praktycznie jedynie rozszerzenia interfejsu. W naszym przypadku pracujemy z bazą danych SQL, więc musimy wykorzystać CrudRepository, a najlepiej JpaRepository.

Jak wcześciej wspomniałem, dużo lepszym wyborem jest JpaRepository. Interfejs ten jest bogatszy i prostszy w użyciu.

package pl.blog.spring.springdata;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<BookEntity, Long> {
}

Interfejs pozostawiamy pusty, musimy jedynie zainteresować się przekazywanymi typami podczas rozszerzania JpaRepository.

Każde repozytorium musi wskazać:

  • encję, dla której tworzymy dane repozytorium,
  • typ klucza głównego (ang.primary key) naszej encji.

Z tego powodu do JpaRepository przekazujemy jako pierwszy parametr typ encji (BookEntity), a jako drugi typ primaryKey (Long).

I to na tyle, resztę załatwia za nas już Spring Data. Podczas uruchomienia aplikacji, Spring Data stworzy implementację wszystkich metod zdefiniowanych w interfejsie.

Tworzenie kontrolera

W BookController będziemy tworzyć kolejno endpointy odpowiadające operacjom CRUD.

@RestController
@RequestMapping("/book")
@RequiredArgsConstructor
public class BookController {
    private final BookRepository bookRepository;
}

Do kontrolera został wstrzyknięty BookRepository, przy użyciu którego będziemy wykonywać operacje CRUD na tabeli books.

findAll()

Metoda List<T> findAll() jest odpowiedzialna za zwrócenie wszystkich, istniejących elementów w bazie danych.

@GetMapping
public ResponseEntity<List<BookEntity>> getAllBooks() {
    return ResponseEntity.ok().body(bookRepository.findAll());
}

findById(ID id)

Metoda Optional<T> findById(ID id) przyjmuje identyfikator encji oraz wyszukuje go w tabeli books. Jeśli go nie znajdzie to zostaje zwrócony pusty Optional. W przypadku, gdy zostanie zwrócony pusty Optional, kontroler ma zwrócić http status 404 – Not Found.

@GetMapping("/{id}")
public ResponseEntity<?> getBookById(@PathVariable Long id) {
    return bookRepository.findById(id)
            .map(ResponseEntity::ok)
            .orElseGet(() -> ResponseEntity.notFound().build());
}

save(T s)

Metoda T save(T t) umożliwia zapisanie encji do bazy danych. Warto zauważyć, że po dodaniu encji do bazy, zostaje ona zwrócona. Dzięki temu, mamy możliwość poznać wygenerowany identyfikator dla encji.

@PostMapping
public ResponseEntity<BookEntity> createBook(@RequestBody CreateBookModel createBookModel) {
    BookEntity bookToCreate = new BookEntity(createBookModel.getTitle(), createBookModel.getAuthorName());
    return ResponseEntity.ok(bookRepository.save(bookToCreate));
}

delete(ID id) & existsById(ID id)

Usuwanie elementu z bazy danych możemy obsłużyć na dwa sposoby.

Pierwszy z nich to sprawdzanie czy element istnieje, a dopiero usunięcie danego elementu.

@DeleteMapping("/{id}")
public ResponseEntity<?> deleteBook(@PathVariable Long id) {
    if (bookRepository.existsById(id)) {
        bookRepository.deleteById(id);
        return ResponseEntity.ok().build();
    }

    return ResponseEntity.notFound().build();
}

Drugim rozwiązaniem jest pominięcie metody existsById. W przypadku, gdy encja nie istnieje, Spring Data rzuci wyjątek EmptyResultDataAccessException (w poprzednich wersjach i nadal wg JavaDoc metoda ta rzuca IllegalArgumentException).

@DeleteMapping("/{id}")
public ResponseEntity<?> deleteBook(@PathVariable Long id) {
    try {
        bookRepository.deleteById(id);
        return ResponseEntity.ok().build();
    } catch (EmptyResultDataAccessException e) {
        return ResponseEntity.notFound().build();
    }
}

Osobiście wolę pierwsze rozwiązanie, ponieważ jak można zauważyć, nigdy nie wiadomo kiedy twórcy ponownie zmienią rzucany wyjątek.

count()

Czasami potrzebujemy liczby istniejących rekordów. Zamiast robić to nieefektywnie poprzez metodę findAll() oraz sprawdzanie rozmiaru tablicy, możemy użyć metody count(). Dzięki temu żadne dane nie są wyciągane z bazy danych, a liczba rekordów jest zliczana natywnie.

@GetMapping("/count")
public ResponseEntity<?> countBooks() {
    return ResponseEntity.ok(bookRepository.count());
}

Co więcej?

Jak wspomniałem na początku, Spring Data jest bardzo rozbudowanym tworem. Ma wiele funkcjonalności, które warto znać i które są bardzo przydatne w ciągłej pracy projektowej.

W artykule chciałem jedynie umieścić podstawowe zagadnienia związane ze Spring Data. Celem było, abyś na początek mógł zrozumieć podstawy oraz potrafił użyć jednego z sub-modułów – czyli Spring Data JPA.

W kolejnych artykułych rozwinę takie tematy jak:

  • Query Method,
  • @Query,
  • paginacja i sortowanie,
  • transakcje.

Podsumowanie

Mam ogromną nadzieję, że ten artykuł pomógł Ci choć w małym stopniu zrozumieć zasadę działania Spring Data. Daj znać w komentarzu czy wcześniej korzystałeś ze Spring Data lub czy jest coś co sprawia Ci problem podczas używania tej biblioteki?

Wszystkie przykłady oraz testy do nich znajdziesz w tym repozytorium.

Kamil Klimek

Kamil Klimek

Pierwszy kalkulator napisany w języku Pascal w podstawówce. Później miałem trochę przygód z frontendem oraz PHP, na studiach poznałem C++ oraz Jave. Obecnie prawie 3 letnie doświadczenie jako Java full stack develop. Blog jest miejscem, dzięki któremu mogę się dzielić wiedzą i pomagać innym w nauce programowania.
Subscribe
Powiadom o
guest
0 komentarzy
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x