https://www.pexels.com/photo/beige-and-purple-beans-176169/

Ebean i CRUD

W jednym z poprzednich wpisów opisywałem swoje problemy związane z integracją Play Framework z JPA (Hibernate). Na ten temat poświęciłem jeszcze chwilę, po czym postanowiłem się nie upierać nad tym rozwiązaniem i skorzystać jednak z EBean. Z Hibernate spotkałem się już wcześniej, natomiast EBean jest dla mnie nowy. Co prawda chciałbym wrócić kiedyś do sparowania Play z Hibernate, bo jest to na pewno popularniejsza implementacja JPA, a więc również większy atut w CV.

Konfigurację EBean wykonałem zgodnie z dokumentacją. Z tych ważniejszych etapów konfiguracji, w pliku plugins.sbt dodałem plugin PlayEbean.

addSbtPlugin("com.typesafe.sbt" % "sbt-play-ebean" % "3.0.0")

W pliku build.sbt dodałem wpis dołączający do projektu zależność umożliwiającą komunikację z silnikiem PostgreSQL oraz aktywujący plugin PlayEbean.

lazy val root = (project in file(".")).enablePlugins(PlayJava, PlayEbean)	 
libraryDependencies += "org.postgresql" % "postgresql" % "42.0.0"

Na koniec konfiguracji został plik application.conf, w którym ustawiłem parametry połączenia z bazą danych. Również w tym miejscu określiłem, w jakim folderze znajdują się klasy modeli.

db {
    default.driver=org.postgresql.Driver
    default.url="jdbc:postgresql://127.0.0.1/homecontroller?characterEncoding=UTF-8&useSSL=true"
    default.username=homecontroller
    default.password="qwerty"
 
    default.logSql=true
}
ebean.default = ["models.*"]

Podczas próby stworzenia encji z modelu napotkałem problemy związane z niewidocznością niezbędnych pakietów (javax.persistence oraz com.avaje.ebean). Rozwiązaniem okazało się wykonanie na projekcie operacji Clean, następnie Reload Project ClassPath i na końcu Build Project.

W tym momencie moje dalsze działania w końcu mogły pójść w stronę implementacji czterech podstawowych operacji związanych z operacjami na bazie danych, czyli CRUD (Create, Read, Update, Delete). Na warsztat wziąłem klasę Room, której obsługa wydawała mi się najprostsza. Przywołam w tym miejscu klasę Room, bo choć kod jest on na GitHub, to tam będzie on ewaluował, a tu niech stanowi snapshota w chwili, w której był pisany ten wpis.

@Entity
@Table(name = "room")
public class Room extends Model {
 
    /**
     * Room ID
     */
    @Id
    @Column
    @GeneratedValue(strategy = GenerationType.AUTO)
    public int id;
 
    /**
     * A name of the room
     */
    @Column(name = "name", unique = true)
    @Constraints.Required(message = "Nazwa pomieszczenia nie może być pusta.")
    @Length(min = 2, max = 20, message = "Nazwa pomieszczenia może mieć od 2 do 20 znaków.")
    @SafeHtml(whitelistType = SafeHtml.WhiteListType.NONE, message = "Nazwa pomieszczenia zawiera niedozwolone znaki.")
    public String name;
 
    /**
     * Detailed description of the room
     */
    @Column(name = "description")
    @Length(min = 0, max = 200, message = "Dodatkowy opis pomieszczenia może mieć maksymalnie 200 znaków.")
    @SafeHtml(whitelistType = SafeHtml.WhiteListType.NONE, message = "Dodatkowy opis pomieszczenia zawiera niedozwolone znaki.")
    public String description;
 
    /**
     * Sensors installed in the room
     */
    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "room")
    public List<Sensor> sensors;
 
    public static Finder<Integer, Room> find = new Finder<Integer, Room>(Room.class);
 
    @Override
    public String toString() {
        return "Room{" + "id=" + id + ", name=" + name + ", description=" + description + ", sensors=" + sensors + '}';
    }
}

Model będzie mapowany na tabelę o nazwie room w bazie danych, co uzyskałem dzięki zastosowaniu adnotacji @Table(name = „room”). Kluczem głównym będzie identyfikator (id), generowany automatycznie przez silnik bazodanowy. Jak widać, instancja tej klasy będzie posiadała swoją nazwę (name) oraz dodatkowy opis (description). W każdym pomieszczeniu będą mogły być zainstalowane sensory, które przechowywane będą w polu sensors. Adnotacja @OneToMany tworzy relację z modelem sensorów (klasa Sensor). Jest również statyczne pole Finder<Integer, Room> find, które będzie upraszczało proces wyszukiwania encji.

Pola name oraz description oznaczone są adnotacjami, które określają ograniczenia dla przypisywanych wartości. Ograniczenia te będą sprawdzane w procesie walidacji przy próbie wiązania danych z formularza do modelu. W tym przykładzie pole name nie może być puste (@Constraints.Required), natomiast oba pola mają ograniczoną długość (@Length) oraz nie dopuszcza się stosowania tagów HTML, a jedynie czystego tekstu (@SafeHtml). Każde ograniczenie ma możliwość przypisania komunikatu błędu, który będzie wyświetlany użytkownikowi w przypadku wykrycia przez walidator błędów. Na tym etapie komunikaty są zaszyte na sztywno w klasie modelu, jednak w przyszłości trafią do wydzielonych plików, co w razie konieczności umożliwi zmiany języka.

Opisywałem już strukturę mojej aplikacji, jednak nie było wtedy nic o implementacji. Warstwę serwisu pominę, gdyż jest to w zasadzie przekierowanie do warstwy danych. Klasa PostgreSQLRoomRepositoryImpl.java będąca implementacją interfejsu RoomRepository zawiera realizację metod CRUD.

Create – dodawanie pomieszczenia (INSERT)

Metoda addRoom() dodaje nowe pomieszczenie do systemu. Przed operacją save() wykonywane jest sprawdzenie, czy pozycja o takiej nazwie nie istnieje już w bazie danych. Warunek jest powiązany z definicją kolumny name, gdzie wymagana jest unikalność tej wartości (@Column(name = „name”, unique = true)). W kolejnym kroku realizacji projektu, tego typu sprawdzenia przeniosę do warstwy serwisu.

    @Override
    public void addRoom(Room room) {
        if (getRoomByName(room.name) == null) {
            room.save();
        } else {
            throw new RoomExistException();
        }
    }

Read – pobieranie informacji (SELECT)

W operacjach pobierania danych wykorzystywane jest wspomniane wcześniej pole typu Finder<I,T>. Na razie utworzyłem trzy metody – pobierająca listę wszystkich pomieszczeń (getAllRooms()), pobierająca wybrane pomieszczenie na podstawie podanego id (getRoomById(int id)) oraz na podstawie nazwy pomieszczenia (getRoomByName(String roomName)).

@Override
    public Room getRoomById(int id) {
        Room room = Room.find.byId(id);
        return room;
    }
 
    @Override
    public Room getRoomByName(String roomName) {
        Room room = Room.find.where()
                .eq("name", roomName)
                .findUnique();
        return room;
    }
 
    @Override
    public List<Room> getAllRooms() {
        List<Room> rooms = Room.find.all();
        return rooms;
    }

Update – zmiana wartości (UPDATE)

Metoda updateRoom(Room room) kasuje z bazy danych pomieszczenie i nie wymaga szerszego komentarza:)

    @Override
    public void updateRoom(Room room) {
        room.update();
    }

Delete – zmiana wartości (DELETE)

Bardzo podobna metoda do tej aktualizującej pomieszczenie. Metoda deleteRoom(int id) kasuje pomieszczenie o podanym id.

    @Override
    public boolean deleteRoom(int id) {
        Room room = getRoomById(id);
        return room.delete();
    }

Powyższe metody wywoływane są z poziomu kontrolera RoomController, za pośrednictwem RoomService (RoomServiceImpl). Przykładowo pobierana jest lista wszystkich pomieszczeń, po czym przekazywana jest do widoku.

    public Result list() {
        List<Room> rooms = roomService.getAllRooms();
        return ok(views.html.rooms.roomsList.render("Lista pomieszczeń", rooms));
    }

Serdecznie zapraszam do śledzenia bloga. Już po weekendzie pojawi się kolejny wpis, tym razem o wykorzystaniu tego całego kodu, o którym była dziś mowa. Utworzę widoki z pierwszymi formularzami, przy okazji wykorzystując operacje CRUD. Równolegle do prac „pisarskich”, będę refaktoryzował kod oraz rozszerzał funkcjonalności związane z dostępem do bazy danych o implementacje dla kolejnych modeli danych.

Literatura obowiązkowa

Zapach świeżej kawy: Play! Framework
Play Framework 2: Ebean vs. JPA
Configuring EBean
Query Features

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *