Wstęp
W dobie powszechności bibliotek służących do mapowania klas na bazę lub bazę na klasy (tak zwanych ORMappingów) powstało wiele rozwiązań implementujących jedno lub drugie rozwiązanie. Jednym z takich rozwiązań jest biblioteka jOOQ, która realizuje drugi z tych typów, tzn. na podstawie stworzonej bazy danych tworzy klasy javowe za pomocą których w łatwy sposób możemy zarządzać danymi. Warto wspomnieć o polityce licencyjnej przyjętej przez twórców biblioteki, mianowicie w przypadku korzystania z darmowych baz danych używanie owej jest darmowe. Natomiast w przypadku baz komercyjnych licencja jest już płatna (jest to całkiem logiczne, skoro stać nas na baze to zapewne też stać nas na bibliotekę). Każdy kto myśli o migracji na baze komercyjną, a chce używać jOOQ powinien wzięć to pod uwagę przy jej wyborze.
Co tak właściwie będzie prezentował artykuł?
Celem artykułu jest pokazanie podstawowych możliwości jOOQa (i oczywiście zachęcenie do wypróbowania) na przykładzie akademickim jakim jest realizacja książki telefonicznej umożliwiającej dodawanie/usuwanie kontaktów wraz z telefonami. Książka ta będzie dostępna z poziomu przeglądarki internetowej. Do realizacji tego zadania będę używał frameworka SpringBoot (zakładam czytelniku, że znasz owy framework lub jemu podobny) który spina cała warstwę aplikacyjną. Za warstwę danych będzie odpowiedzialna dobra darmowa baza jaką jest PostgreSQL (w przykładam korzystam z wersji 9.3). Do budowania projektu będę używać oprogramowania Maven. A w końcu do "zaprojektowania" bazy wraz z wygenerowaniem zapytań SQL będę używać świetnej aplikacji chmurowej jaką jest Vertabelo.
Instalacja środowiska
Przestawię poniżej przykładową konfigurację dla dystrybucji Ubuntu, myślę, że w przypadku innych dystrybucji oraz systemu Windows realizacja będzie podobna.
Instalacja PostgreSql:
sudo apt-get install postgresql postgresql-contrib
Konfiguracja PostgreSql:
W pliku
/etc/postgresql/<numer_wersji>/main/pg_hba.conf
zamienic wszystkie metody na
trust
.
Instalacja Maven:
sudo apt-get install maven
Konfiguracja środowiska niezbędną do realizacji projektu:
W katalogu gdzie chcemy przechowywać źródłą naszego projektu tworzymy następujący plik
pom.xml
4.0.0
pl.mostua
jooq-tutorial
0.0.1
org.springframework.boot
spring-boot-starter-parent
1.2.3.RELEASE
org.springframework.boot
spring-boot-starter-web
1.8
org.springframework.boot
spring-boot-maven-plugin
służący programowi maven do budowania środowiska.
Należy też utworzyć odpowiednie pliki java:
src/main/java/jooq_example/TelephoneBookController.java
package jooq_example;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController
public class TelephoneBookController {
@RequestMapping("/")
public String myPhones() {
return "Ksiazka kontaktowa";
}
}
src/main/java/jooq_example/Application.java
package jooq_example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import java.sql.Connection;
import java.sql.DriverManager;
@SpringBootApplication
@Configuration
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
}
}
Aby uruchimić to co udało nam się stworzyć należy w katalogu głównym projektu wykonąć polecenie
mvn package && java -jar target/jooq-tutorial-0.0.1.jar
W przeglądarce pod adresem
localhost:8000
powinniśmy ujrzeć naszą stronę internetową.
Teraz możemy przejść do meritum artykułu.
Warstwa danych
Projekt bazy danych zamodelowany w aplikacji Vertabelo wraz z sekwencjami zaczynającymi się od 500, aby można było dodać dane testowe:
Oraz skrypty wygenerowane przez nań.
sql/jooq_tutorial_create.sql:
-- Created by Vertabelo (http://vertabelo.com)
-- Last modification date: 2015-05-17 18:24:18.697
-- tables
-- Table: friend
CREATE TABLE friend (
id int NOT NULL,
name varchar(255) NOT NULL,
surname varchar(255) NOT NULL,
CONSTRAINT friend_pk PRIMARY KEY (id)
);
-- Table: phone_number
CREATE TABLE phone_number (
id int NOT NULL,
phone varchar(13) NOT NULL,
friend_Id int NOT NULL,
CONSTRAINT phone_number_pk PRIMARY KEY (id)
);
-- foreign keys
-- Reference: phone_number_friend (table: phone_number)
ALTER TABLE phone_number ADD CONSTRAINT phone_number_friend
FOREIGN KEY (friend_Id)
REFERENCES friend (id)
NOT DEFERRABLE
INITIALLY IMMEDIATE
;
-- sequences
-- Sequence: friend_id_seq
CREATE SEQUENCE friend_id_seq
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
START WITH 500
NO CYCLE
;
-- Sequence: phone_number_id_seq
CREATE SEQUENCE phone_number_id_seq
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
START WITH 500
NO CYCLE
;
-- End of file.
Ponadto warto dodać kilka danych testowych.
sql/jooq_tutorial_create.sql:
INSERT INTO friend VALUES (1, 'Adam', 'Kowalski');
INSERT INTO friend VALUES (2, 'Michał', 'Nowak');
INSERT INTO phone_number VALUES(1, '333222111', 1);
INSERT INTO phone_number VALUES(2, '222333111', 1);
Aby stworzyć bazę która będzie przechowywać nasze dane oraz wykonać powyższe skrypty należy wykonać poniższe polecenie w katalogu głównym projektu
createdb jooq_tutorial -U postgres
psql jooq_tutorial postgres -f sql/jooq_tutorial_create.sql
psql jooq_tutorial postgres -f sql/jooq_tutorial_add.sql
Żeby sprawdzić czy wszystko się udało możemy się zalogować do bazy:
psql jooq_tutorial postgres
oraz wpisać
SELECT * FROM friends;
Co pokaże czy z sukcesem udało nam się dodać dane testowe.
Konfiguracja jOOQa
Należy zmodyfkować plik pom.xml w następujący spsób:
<?xml version="1.0" encoding="UTF-8"?>
4.0.0
pl.mostua
jooq-tutorial
0.0.1
org.springframework.boot
spring-boot-starter-parent
1.2.3.RELEASE
org.springframework.boot
spring-boot-starter-web
org.jooq
jooq
3.4.5
org.jooq
jooq-meta
3.4.5
org.jooq
jooq-codegen
3.4.5
org.postgresql
postgresql
9.3-1102-jdbc41
1.8
org.springframework.boot
spring-boot-maven-plugin
org.jooq
jooq-codegen-maven
3.4.5
generate
org.postgresql
postgresql
9.3-1102-jdbc41
org.postgresql.Driver
jdbc:postgresql://localhost:5432/jooq_tutorial
postgres
test
org.jooq.util.DefaultGenerator
org.jooq.util.postgres.PostgresDatabase
.*
public
jooq_sources
src/main/java
true
Wyjaśnienie:
Do zależności dodajemy wszystkie zależności komponentów jOOQa oraz sterownik bazy (sekcja
<dependencies>
). Natomiast w sekcji
<plugins>
definiujemy plugin
jooq-codegen-maven
, który wygeneruje nam klasy na podstawie bazy. Zleży on od sterownika bazy (sekcja
<dependency>
), tę należy zmienić gdy korzystamy z innej. W sekcji
<configuration>
określamy parametry połączenia z których będzie korzystał plugin (jeżeli ustawimy metodę uwierzytelnienia na
trust
to hasło nie ma znaczenia, warto o tym pamiętać). W sekcji
<generator>
określamy typ bazy, to gdzie kod ma być wygenerowany (<target>) oraz, że chcemy wygenerować obiekty DAO (o owych jeszcze wspomnę).
Teraz po wykonaniu polecenia
mvn package
w katalogu głównym projektu powinien pojawić się folder
src/main/java/jooq_sources
w którym plugin jooqa wygenerował odpowiednie klasy.
Implementacja
Zacznijmy od modyfikacji pliku
Application tak, żeby dostarczał on odpowiednich beanów.
package jooq_example;
import org.jooq.SQLDialect;
import org.jooq.impl.DefaultConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.sql.Connection;
import java.sql.DriverManager;
@SpringBootApplication
@Configuration
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
}
@Autowired
private org.jooq.Configuration configuration;
@Bean(name = "dbConfiguration")
public org.jooq.Configuration getConfiguration() {
return new DefaultConfiguration().set(connection).set(SQLDialect.POSTGRES);
}
@Bean
public TelephoneBookManager getTelephoneBookManager() {
return new TelephoneBookManagerImpl(configuration);
}
@Bean
public Connection getConnection() {
String userName = "postgres";
String password = "test";
String url = "jdbc:postgresql://localhost:5432/jooq_tutorial";
try {
return DriverManager.getConnection(url, userName, password);
} catch (Exception e) {
throw new RuntimeException("Cannot connect to database");
}
}
}
Powyższe beany to
- Configuration wymagany przez jOOQ, jest to informacja o bazie jak i połączeniu.
- Connection slużący do połaczenia się z bazą.
- Implementacje interfejsu TelephoneBookManager realizującego logikę biznesową aplikacji.
Teraz możemy przystąpić do zdefinjowaniu interfejsu TelephoneBookManager realizującego logikę biznesową:
package jooq_example;
import jooq_sources.tables.pojos.Friend;
import jooq_sources.tables.pojos.PhoneNumber;
import java.util.List;
/**
* Realizuje logike biznesową związana z dodawniem telefonów
*
* @author Adam Mościcki
*/
public interface TelephoneBookManager {
/**
* Pobiera listę znajomych
* @return
*/
List<Friend> getFriends();
/**
* Dodanie znajmego
* @return
*/
void addFriend(Friend friend);
/**
* Dodanie znajmego w raz z numerem telefonu
* @return
*/
void addFriendWithPhoneNumber(Friend friend, String phoneNumber);
/**
* Pobiera listę znajomych
* @return
*/
void removeFriend(Integer friendId);
/**
* Zwraca listę telefonów danego użytkownika
* @param friendId
* @return lista telefonów
*/
List<PhoneNumber> getFriendPhones(Integer friendId);
/**
* Dodanie numeru telefonu dla danego użytkownika
*/
void addPhoneToFriend(Integer friendId, String phoneNumber);
/**
* Usuniecie numeru telefonu
*/
void removeNumber(Integer phoneId);
}
Oraz implementacje TelephoneBookManagerImpl (tutaj właśnie jOOQ lśnić będzie):
package jooq_example;
import jooq_sources.Sequences;
import jooq_sources.Tables;
import jooq_sources.tables.daos.FriendDao;
import jooq_sources.tables.daos.PhoneNumberDao;
import jooq_sources.tables.pojos.Friend;
import jooq_sources.tables.pojos.PhoneNumber;
import org.jooq.Configuration;
import org.jooq.DSLContext;
import org.jooq.TransactionalRunnable;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
/**
* @author Adam Mościcki
*/
public class TelephoneBookManagerImpl implements TelephoneBookManager{
private final Configuration configuration;
private final FriendDao friendDao;
private final PhoneNumberDao phoneNumberDao;
@Autowired
public TelephoneBookManagerImpl(Configuration configuration) {
this.configuration = configuration;
this.friendDao = new FriendDao(configuration);
this.phoneNumberDao = new PhoneNumberDao(configuration);
}
@Override
public List<Friend> getFriends() {
return friendDao.findAll();
}
@Override
public void addFriend(Friend friend) {
//addFriendByDirectlySet(friend);
//addFriendByDao(friend);
addFriendDirectlySimple(friend);
}
private void addFriendByDao(Friend friend) {
DSLContext create = DSL.using(configuration);
Long nextId = create.nextval(Sequences.FRIEND_ID_SEQ);
friend.setId(nextId.intValue());
friendDao.insert(friend);
}
private void addFriendByDirectlySet(Friend friend) {
DSLContext create = DSL.using(configuration);
create.insertInto(Tables.FRIEND)
.set(Tables.FRIEND.ID, Sequences.FRIEND_ID_SEQ.nextval().cast(Tables.FRIEND.ID))
.set(Tables.FRIEND.NAME, friend.getName())
.set(Tables.FRIEND.SURNAME, friend.getSurname())
.execute();
}
private void addFriendDirectlySimple(Friend friend) {
DSLContext create = DSL.using(configuration);
create.insertInto(Tables.FRIEND)
.values(Sequences.FRIEND_ID_SEQ.nextval(), friend.getName(), friend.getSurname())
.execute();
}
@Override
public void addFriendWithPhoneNumber(Friend friend, String phoneNumber) {
addFriendWithPhoneNumberClassic(friend, phoneNumber);
//addFriendWithPhoneNumberLambda(friend, phoneNumber);
}
private void addFriendWithPhoneNumberLambda(Friend friend, String phoneNumber) {
DSLContext create = DSL.using(configuration);
create.transaction(configuration -> {
DSLContext createInner = DSL.using(configuration);
Integer id = createInner.insertInto(Tables.FRIEND)
.values(Sequences.FRIEND_ID_SEQ.nextval(), friend.getName(), friend.getSurname())
.returning(Tables.FRIEND.ID).fetchOne().getId();
createInner.insertInto(Tables.PHONE_NUMBER)
.values(Sequences.PHONE_NUMBER_ID_SEQ.nextval(), phoneNumber).execute();
});
}
private void addFriendWithPhoneNumberClassic(final Friend friend, final String phoneNumber) {
DSLContext create = DSL.using(configuration);
create.transaction(new TransactionalRunnable() {
@Override
public void run(Configuration configuration) throws Exception {
DSLContext create = DSL.using(configuration);
Integer id = create.insertInto(Tables.FRIEND)
.values(Sequences.FRIEND_ID_SEQ.nextval(), friend.getName(), friend.getSurname())
.returning(Tables.FRIEND.ID).fetchOne().getId();
create.insertInto(Tables.PHONE_NUMBER)
.values(Sequences.PHONE_NUMBER_ID_SEQ.nextval(), phoneNumber, id).execute();
}
});
}
@Override
public void removeFriend(Integer friendId) {
DSLContext create = DSL.using(configuration);
create.transaction(configuration -> {
DSLContext createInner = DSL.using(configuration);
createInner.delete(Tables.PHONE_NUMBER)
.where(Tables.PHONE_NUMBER.FRIEND_ID.equal(friendId))
.execute();
friendDao.deleteById(friendId);
});
}
@Override
public List<PhoneNumber> getFriendPhones(Integer friendId) {
return phoneNumberDao.fetchByFriendId(friendId);
}
@Override
public void addPhoneToFriend(Integer friendId, String phoneNumber) {
phoneNumberDao.insert(new PhoneNumber(DSL.using(configuration).nextval(Sequences.PHONE_NUMBER_ID_SEQ).intValue(),
phoneNumber, friendId));
}
@Override
public void removeNumber(Integer phoneId) {
phoneNumberDao.deleteById(phoneId);
}
}
Wyjaśnienia
Powyższy kod będę stopniowo analizował zaczynając od rzeczy bardzo podstawowych skończywszy na tych bardzo przydatnych.
Interfejsy DSLContext, Configuration oraz obiekt DSL
Obiekt implementujący interfejs
Configuration
przechowuje w sobie informacje o połączeniu z bazą oraz dialekcie - typie bazy. Obiekt implementujący interfejs
DSLContext
przechowuje dowiązany kontekst z konkretną konfiguracją, za pomocą tego obiektu możemy już tworzyć konkretne zapytania. Obiekt
DSL
m.in. zwraca implementacje poszczególnych interfejsów np.
DSLContext
na podstawie
Configuration
z czego będziemy często korzystać. Mam nadzieje, że na poniższych przykładach trochę to się rozjaśni.
Dodanie danych
@Override
public void addFriend(Friend friend) {
//addFriendByDirectlySet(friend);
//addFriendByDao(friend);
addFriendDirectlySimple(friend);
}
private void addFriendByDao(Friend friend) {
DSLContext create = DSL.using(configuration);
Long nextId = create.nextval(Sequences.FRIEND_ID_SEQ);
friend.setId(nextId.intValue());
friendDao.insert(friend);
}
private void addFriendByDirectlySet(Friend friend) {
DSLContext create = DSL.using(configuration);
create.insertInto(Tables.FRIEND)
.set(Tables.FRIEND.ID, Sequences.FRIEND_ID_SEQ.nextval().cast(Tables.FRIEND.ID))
.set(Tables.FRIEND.NAME, friend.getName())
.set(Tables.FRIEND.SURNAME, friend.getSurname())
.execute();
}
private void addFriendDirectlySimple(Friend friend) {
DSLContext create = DSL.using(configuration);
create.insertInto(Tables.FRIEND)
.values(Sequences.FRIEND_ID_SEQ.nextval(), friend.getName(), friend.getSurname())
.execute();
}
Zaimplementowałem metodę
addFriend
na 3 równoważne sposóby:
- addFriendDirectlySet
Za pomocą metody insertInto
na obiekcie kontekstowym przyjmującej "identyfikator" tabeli wygenerowany przez jOOQa mówimy, że do tabeli friend
dla atrybutu 'id' ustawiamy id pobierane z sekwencji, imię użytkownika na te z obiektu friend, nazwisko na te z obiektu friend, po czym dla tych ustawień wykonujemy zbudowane zapytanie. Jest to równoważne z zapytaniem:
INSERT INTO friend (id,name, surname) VALUES (friend_id_seq.NEXTVAL,'imie','nazwisko');
Pobranie z sekwencji następnej wartości realizowane jest poprzez odniesienie do obiektu
Sequences.FRIEND_ID_SEQ
i jego metody
nextval()
, co dalej jest to rzutowane to typu atrybutu id. Ponadto jeżeli chcielibyśmy pobrać id najpierw, a później je ustawić to wycięcie tej części na żywca nie zadziała, bo obiekt
Sequences.FRIEND_ID_SEQ
nie wiedziałby o kontekście w jakim jest realizowany, jak to zrobić pokaże poniżej.
addFriendDirectlySimple
To rozwiązanie robi dokładnie to samo tylko odpowiada sqlowi następującemu:
INSERT INTO friend VALUES (friend_id_seq.NEXTVAL,'imie','nazwisko');
Co przy wielu atrybutach staje sie mało czytelne
addFriendByDao
Ostatnia metoda ustawia zmiennej
friend
id i poprzez obiekt DAO (Data Access Object), wstawia go do bazy. Obiekty DAO udostępniają podstawowe metody typu wstawianie, usuwanie, wyszukiwanie przez co upraszczają cała logikę.
Wyciągniecie następnej wartości z sekwencji realizujemy poprzez wywołanie metody
nextval
oczkującej obiektu sekwencji na obiekcie kontekstowym.
Tranzakcja, returning
@Override
public void addFriendWithPhoneNumber(Friend friend, String phoneNumber) {
addFriendWithPhoneNumberClassic(friend, phoneNumber);
//addFriendWithPhoneNumberLambda(friend, phoneNumber);
}
private void addFriendWithPhoneNumberLambda(Friend friend, String phoneNumber) {
DSLContext create = DSL.using(configuration);
create.transaction(configuration -> {
DSLContext createInner = DSL.using(configuration);
Integer id = createInner.insertInto(Tables.FRIEND)
.values(Sequences.FRIEND_ID_SEQ.nextval(), friend.getName(), friend.getSurname())
.returning(Tables.FRIEND.ID).fetchOne().getId();
createInner.insertInto(Tables.PHONE_NUMBER)
.values(Sequences.PHONE_NUMBER_ID_SEQ.nextval(), phoneNumber).execute();
});
}
private void addFriendWithPhoneNumberClassic(final Friend friend, final String phoneNumber) {
DSLContext create = DSL.using(configuration);
create.transaction(new TransactionalRunnable() {
@Override
public void run(Configuration configuration) throws Exception {
DSLContext create = DSL.using(configuration);
Integer id = create.insertInto(Tables.FRIEND)
.values(Sequences.FRIEND_ID_SEQ.nextval(), friend.getName(), friend.getSurname())
.returning(Tables.FRIEND.ID).fetchOne().getId();
create.insertInto(Tables.PHONE_NUMBER)
.values(Sequences.PHONE_NUMBER_ID_SEQ.nextval(), phoneNumber, id).execute();
}
});
}
Powyższa metoda addFriendWithPhoneNumber
realizuje dodanie użytkownika wraz z numerem telefonów co jest równoważne operacji na dwóch tabelach, stąd uzasadnione jest robienie obu wstawień w obrębie transakcji, ponadto to wymaganie umożliwia pokazanie funkcjonalności Postgresa (oraz jej obsługi przez jOOQ) jaką jest komenda returning umożliwiająca zwrócenie wartości (zbioru wartości) wstawianej (w naszym przypadku id użytkownika, które jest kluczem obcym dla numeru telefenu).
Powyższa metoda addFriendWithPhoneNumber
, posiada 2 równoważne implementacje, jedna wykorzystująca nowość w javie 8, tzn. wyrażenia lambda (addFriendWithPhoneNumberLambda
) operującego na obiekcie implementującym interfejs Configuration
, druga wykorzystująca stary mechanizm tzn. anonimową implementacje interfejsu(addFriendWithPhoneNumberClassic
) przeciązająca metodę run(Configuration)
. W jednym jak i drugim przypadku w obrębie metody transaction
wykonujemy naszą transakcje. Aby skorzystać z komendy RETURNING, wystarczy że wykonamy metodę .returning(Tables.FRIEND.ID).fetchOne().getId();
tzn. określimy atrybut, a następnie go pobierzemy, co w prosty sposób załatwia nam pobranie danego atrybutu (w tym przypadku id).
Pełna implementacja
Aby nie zaciemniać tematu nie będę wstawiał tutaj implementacji Springowej, która po prostu wywołuje odpowiednie metody menadżera, natomiast drogi Czytelniku jeżeli chcesz to zachęcam do pobrania gotowego kodu oraz jego uruchamianie (ostrzegam, że jest on kodem przykładowym, a więc nie waliduje mnóstwa rzeczy, które powinny być sprawdzona w kodzie produkcyjnym).
Implementacja
Podsumowanie
Starałem się pokazać jaka moc drzemie w ORMapingu jakim jest jOOQ jednocześnie prezentując prosty poradnik dla kogoś kto chciałby jej użyć "na już". Warto jednak pamiętać, że jego możliwości daleko wykraczają poza ten wpis, a więc wymagają pewnego wkładu aby je poznać (do czego gorąco zachęcam). A na koniec dla wszystkich zainteresowanych bazami danych polecam odwiedzenie twitera twórcy biblioteki
https://twitter.com/lukaseder.
Bibliografia
http://www.jooq.org/
http:/vertabelo.com/
http://spring.io/guides/gs/spring-boot/