Play Framework – Ebean jako ORM
W poprzednim wpisie pokazałem w jaki sposób zbudować w pełni funkcjonalne, proste API restowe z użyciem Frameworka Play.
W tym i kolejnych wpisach pokażę w jaki sposób połączyć się z bazą danych i jak użyć wszystkich fajnych funkcji tego nowoczesnego frameworka.
Wpisy podzielę na kilka części:
- baza danych – to ten wpis
- akka – opublikowane – zajrzyj tutaj
- pule wątków – w przygotowaniu
- obsługa błędów – w przygotowaniu
- testy – w przygotowaniu
Po tych wpisach zobaczycie, że można dość prosto napisać dobrze skalującą się aplikację, która będzie działała szybko bez względu na to, czy będzie jeden odwiedzający, czy będzie ich wielu.
Wymagania
- Czysty projekt z frameworkiem Play! w wersji 2.6.x
- Podstawowa znajomość JPA
- Podstawowa znajomość programowania funkcyjnego w Java 8
- 15 minut wolnego czasu
Konfiguracja
Aby używać bazy danych, musimy jakąś mieć 🙂
Użyjemy bazy H2
Jest ona bardzo wygodna dla prostych potrzeb developerskich, a dodatkowo, potrafi działać jako embeded database, która rezyduje w pamięci, a więc nic nie trzeba instalować, a po każdym włączeniu/wyłączeniu aplikacji, mamy nową bazę danych.
Użyjemy też Ebean’a jako ORM’a. Nie będę tutaj udowadniał, czy jest on lepszy czy gorszy od Hibernate’a. Jest trochę inny, ma kilka fajnych funkcji, a wydajnościowo nie jest ani lepszy, ani gorszy od innych ORMów.
Najpierw dodajmy Ebeana:
Do pliku project/plugins.sbt dodajemy linijkę:
1 |
addSbtPlugin("com.typesafe.sbt" % "sbt-play-ebean" % "4.0.2") |
Mamy już Ebean’a na pokładzie. Trzeba go jeszcze włączyć:
W pliku build.sbt mamy linijkę:
1 |
lazy val root = (project in file(".")).enablePlugins(PlayJava) |
Zamieniamy ją na:
1 |
lazy val root = (project in file(".")).enablePlugins(PlayJava, PlayEbean) |
Dodatkowo musimy dodać kilka zależności do pliku build.sbt:
1 2 |
libraryDependencies += javaJdbc libraryDependencies += "com.h2database" % "h2" % "1.4.194" |
Co do tej pory wykonaliśmy:
- Dodaliśmy plugin ebean
- Włączyliśmy obsługę ebean w projekcie
- Dodaliśmy biblioteki odpowiedzialne za obsługę JDBC oraz H2
Model
Czas zdefiniować jakąś tabelkę, coś co będzie w bazie danych.
Utworzymy pakiet models, a w nim dodamy klasę Person:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package models; import io.ebean.Finder; import io.ebean.Model; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import java.util.List; @Entity public class Person extends Model { @Id @GeneratedValue public Long id; public String email; public String name; public String lastName; } |
Standardowe adnotacje JPA nie powinny być zagadką, więc ich nie opisuję.
W oczy rzuca się to, że klasa extenduje Model.
Model to klasa Ebean’a, która dodaje do tej klasy całą magię związaną z obsługą bazy danych.
Finder
Ebean udostępnia Finder. Jest to obiekt, dzięki któremu odpytujemy naszą bazę danych.
Definiujemy go dla każdej klasy osobno. W przypadku Person, będzie to:
1 |
public static final Finder<Long,Person> FINDER = new Finder<>(Person.class); |
Pierwszy parametr to typ klucza głównego. W naszym przypadku jest to Long.
Drugi parametr to klasa, którą opisujemy.
Za pomocą Finder’a możemy odpytywać bazę danych. Opis wszystkich metod znacznie wykracza poza ramy tego wpisu, więc opiszę tylko podstawowe metody, jakie są używane w większości przypadków 😉
- Wyszukanie identyfikatora
123public static Person findById(Long id) {return FINDER.ref(id);} - Wyszukanie po nazwie pola:
123public static List<Person> findByName(String name) {return FINDER.query().where().eq("name",name).findList();} - Wyszukanie wszystkiego:
12345public static List<Person> findAll() {return FINDER.query().findList();}
Oczywiście ebean potrafi dużo więcej, ale jak widać, zasada jest taka: FINDER.query().where()…….findList()
To teraz po wszystkich dodatkowych metodach, nasza klasa Person wygląda tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
package models; import io.ebean.Finder; import io.ebean.Model; import play.libs.F; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import java.util.List; @Entity public class Person extends Model { @Id @GeneratedValue public Long id; public String email; public String name; public String lastName; public static final Finder<Long,Person> FINDER = new Finder<>(Person.class); public static Person findById(Long id) { return FINDER.ref(id); } public static List<Person> findAll() { return FINDER.query().findList(); } public static List<Person> findByName(String name) { return FINDER.query().where().eq("name",name).findList(); } } |
Json
Nie będziemy przyjmowali bezpośredniego modelu jako Json’a. Potrzebujemy do tego osobnej klasy.
Są oczywiście przykłady, które operują bezpośrednio na modelach, ale ja wolę starą szkołę operowania na DTO.
Utworzymy pakiet json i dodamy do niego klasę PersonJson:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package json; public class PersonJson { public Long id; public String email; public String name; public String lastName; public PersonJson(Long id, String email, String name, String lastName) { this.id = id; this.email = email; this.name = name; this.lastName = lastName; } public PersonJson() { } } |
Nic strasznego 🙂
I jeszcze konwerter, który dokona konwersji Person – PersonJson:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package converters; import json.PersonJson; import models.Person; import javax.inject.Singleton; import java.util.function.Function; @Singleton public class PersonToPersonJson implements Function<Person, PersonJson> { @Override public PersonJson apply(Person person) { return new PersonJson( person.id, person.email, person.name, person.lastName); } } |
Proste 🙂
Kontroler
Tutaj zepniemy to wszystko w całość. Już prawie koniec.
Chcemy poprzez api wykonać operacje:
- dodania osoby
- pobrania osób
- pobrania osoby po id
Nic prostszego. Mamy wszystko co potrzebujemy, aby to zrobić, a z poprzedniego wpisu, wiemy jak, pozostaje nudna implementacja:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
package controllers; import converters.PersonToPersonJson; import json.PersonJson; import models.Person; import play.libs.Json; import play.mvc.Controller; import play.mvc.Result; import javax.inject.Inject; import javax.inject.Singleton; import java.util.Optional; import java.util.stream.Collectors; @Singleton public class PersonController extends Controller { @Inject private PersonToPersonJson personToPersonJson; public Result findById(Long id) { return Optional.ofNullable(Person.findById(id)).map(personToPersonJson).map(json -> ok(Json.toJson(json))).orElse(notFound()); } public Result save() { PersonJson personJson = Json.fromJson(request().body().asJson(),PersonJson.class); Person person = new Person(); person.email = personJson.email; person.lastName = personJson.lastName; person.name = personJson.name; person.save(); return ok(Json.toJson(personToPersonJson.apply(person))); } public Result findAll() { return ok(Json.toJson(Person.findAll().stream().map(personToPersonJson).collect(Collectors.toList()))); } } |
Zdefiniowaliśmy kontroler, wstrzyknęliśmy konwerter, i utworzyliśmy kilka metod.
Jedyna uwaga co do zapisu.
W momencie jak rozszerzyliśmy klasę Person o Model, dostaliśmy od Ebean’a metody save(), update(), delete(). Chyba nie muszę opisywać, co one robią 🙂
routes
Jeszcze ustalmy ścieżki:
plik routes:
1 2 3 |
GET /person controllers.PersonController.findAll POST /person controllers.PersonController.save GET /person/:id controllers.PersonController.findById(id:Long) |
Konfiguracja bazy danych
Na sam koniec – musimy jeszcze skonfigurować samo połączenie do bazy danych i powiedzieć gdzie trzymamy klasy modelowe.
Otwieramy application.conf i wpisujemy tam:
1 2 3 |
ebean.default = ["models.*"] db.default.driver=org.h2.Driver db.default.url="jdbc:h2:mem:play" |
To wszystko.
Uruchamiamy aplikację, po pierwszym wprowadzeniu dowolnego adresu w przeglądarce, dostaniemy komunikat, że baza danych nie jest zainicjowana. Play sam się zajmuje ewolucjami baz danych i jeśli nie skonfigurujemy inaczej, to musimy zatwierdzić ewolucję. Potem już wszystko leci jak trzeba 🙂 Konfiguracja ewolucji jest pięknie opisana tutaj
W kolejnym wpisie zajmę się przerobieniem naszej aplikacji na bardziej reactive, rozdzielimy trochę logikę i uporządkujemy kod.