Projekt bilet #2 — konfiguracja bazy danych

By 3 July 2017 Projekt Bilet

W lekcji drugiej skon­fig­u­ru­je­my połącze­nie z bazą danych pod kątem środowiska pro­duk­cyjnego, a także prze­jmiemy kon­trolę nad jej tworze­niem i mody­fikac­ja­mi za pomocą narzędzia Liquibase.

Do tej pory w naszym kur­sie Javy pokazy­wal­iśmy już, jak połączyć się z bazą danych oraz pozostaw­ić jej zarządzanie w rękach automaty­cznych bib­liotek (JPA). Nieste­ty w przy­pad­ku więk­szych pro­jek­tów to pode­jś­cie nie jest ide­alne — dlat­ego dzisi­aj pokaże­my jak ‘odzyskać’ nad nią kon­trolę. Rozdzie­limy także bazę danych testową od tej pro­duk­cyjnej, aby móc bez obaw manip­u­lować dany­mi np. pod­czas testów.

Profile aplikacji

We wstępie wspom­i­nal­iśmy o rozróżnie­niu pomiędzy środowiskiem pro­duk­cyjnym a testowym / devel­op­er­skim. Oczy­wiś­cie zależy nam na rozwiąza­niu, które poz­woli w wygod­ny sposób rozdzielić kon­fig­u­rację takich środowisk i uniknąć wsze­chobec­nych kon­strukcji if (…) then {…} . W Springu takim narzędziem są właśnie profile.

Pro­file to sposób na uporząd­kowanie kon­fig­u­racji i struk­tu­ry aplikacji oraz wprowadze­nie dodatkowych warunk­ów jej dzi­ała­nia — np. inicjowa­nia określonych beanów tylko, jeżeli jak­iś pro­fil jest akty­wny. Jed­nocześnie może być akty­wnych wiele pro­fili, może też nie być akty­wny żaden (jest to domyśl­ny stan dzi­ała­nia aplikacji). Pro­fili nie trze­ba spec­jal­nie deklarować — wystar­czy określony pro­fil ‘włączyć’, aby był akty­wny (nawet jeśli nie zmieni on dzi­ała­nia aplikacji) — w pewnym sen­sie są więc po pros­tu etyki­etka­mi opisu­ją­cy­mi środowisko, w jakim dzi­ała aplikacja.

Pro­file pozwala­ją nam zasad­nic­zo na dwie rzeczy — dodatkowo wczy­tać określoną kon­fig­u­rację (np. pli­ki prop­er­ties lub yaml) lub spowodować, że określony bean zostanie uru­chomiony w aplikacji. Co ważne — nieza­leżnie od akty­wnych pro­fili, domyśl­na kon­fig­u­rac­ja także zostanie wczy­tana (doty­czy to w szczegól­noś­ci ustaw­ień aplikacji — pro­file mogą jedynie nad­pisać ist­niejące ustaw­ienia, ale nie mogą ich usuwać czy dezaktywować).

Aktywowanie profili

Spring pozwala na aktywac­je pro­fili na dwa pod­sta­wowe sposo­by — w kodzie, korzys­ta­jąc z XX zan­im aplikac­ja zostanie uru­chomiona, oraz za pomocą zmi­en­nej środowiskowej spring.profiles.active. O ile oba sposo­by są funkcjon­al­nie iden­ty­czne, pole­camy korzys­tać raczej z kon­fig­u­racji poprzez środowisko — w prze­ci­wnym razie łat­wo przeoczyć określony sposób uruchami­a­nia aplikacji (np. w pro­jek­cie webowym doda­jąc frag­ment kodu do metody main() naszej aplikacji, nie zostanie on wyko­nany jeśli aplikację uru­chomimy w postaci pliku WAR).

W kodzie

Do wyboru mamy dwie opc­je — pier­wsza musi być uży­ta zan­im aplikac­ja będzie uru­chomiona i pole­ga na wywoła­niu poniższego kodu:

SpringApplication.setAdditionalProfiles("development", "localdevelopment");

dru­ga, w już dzi­ała­jącej aplikacji, wyma­ga dostępu do Appli­ca­tion­Con­text (uwa­ga! w zależnoś­ci kiedy ten kod się wykona, może on nie mieć zamier­zonych efek­tów; aby upewnić się, że wykonu­je się on przy star­cie aplikacji, zobacz np ten wątek na SO):

ApplicationContext ctx = null;
((ConfigurableEnvironment)ctx.getEnvironment()).addActiveProfile("development");

W testach

Kole­jną bard­zo przy­daną funkcją jest uży­wanie osob­nych pro­fili do testów — dzię­ki temu może­my jeszcze dokład­niej kon­trolować środowisko testowe, a także uży­wać odd­ziel­nej kon­fig­u­racji (np. innych kluczy do API itp). W przy­pad­ku testów wystar­czy dodać adno­tację @ActiveProfiles nad naszą klasą z tes­ta­mi, np:

@ActiveProfiles({"development", "localdevelopment"})
@SpringBootTest
public class ApplicationTests {
...
}

W środowisku

Aby ustaw­ić akty­wne pro­file na poziomie sys­te­mu oper­a­cyjnego, moż­na ustaw­ić zmi­en­ną środowiskową SPRING_PROFILES_ACTIVE — jest to szczegól­nie użyteczne w przy­pad­ku środowisk pro­duk­cyjnych (opro­gramowanie do obsłu­gi chmury częs­to pozwala kon­fig­urować zmi­enne środowiskowe w prosty sposób — np. AWS BeanStalk lub EC2). Jeśli samodziel­nie kon­fig­u­ru­jesz ser­w­er (lub przy­go­towu­jesz skryp­ty star­towe), w przy­pad­ku sys­temów *nix wystar­czy wywołać komendę:

export SPRING_PROFILES_ACTIVE=production,remoteproduction

aby akty­wować pro­file pro­duc­tion oraz remotepro­duc­tion.

W przy­pad­ku uruchami­a­nia aplikacji samodziel­nie, w zależnoś­ci od sposobu uruchami­a­nia aplikacji, możesz po pros­tu przekazać parametr
spring.profiles.active, np w wier­szu poleceń

mvn spring-boot:run --spring.profiles.active=development,localdevelopment

lub (jeśli uruchami­amy aplikację z pliku JAR)

java -jar -Dspring.profiles.active=development,localdevelopment app.jar

Powyższe zna­jdziesz także w doku­men­tacji Springa

W IDE

Kon­fig­u­rac­ja w IDE jest równie pros­ta i sprowadza się do ustaw­ienia odpowied­nich zmi­en­nych przy uruchami­a­n­iu. W przy­pad­ku Eclipse, do sekcji ‘VM argu­ments’ dodajemy

-Dspring.profiles.active=development,localdevelopment

nato­mi­ast w przy­pad­ku Intel­liJ IDE moż­na sko­rzys­tać z opcji ‘Envi­ron­ment vari­ables’ w oknie kon­fig­u­racji uruchami­a­nia.

Korzystanie z profili w aplikacji

Oczy­wiś­cie sama aktywac­ja pro­fili na niewiele się przy­da, jeśli nasza aplikac­ja nie będzie owych pro­fili wyko­rzysty­wała — jak wspom­i­nal­iśmy wyżej, są dwa sposo­by, na które moż­na użyć profili.

Dodatkowe pliki z ustawieniami

Pier­wszy sposób to nad­pisy­wanie ist­nieją­cych ustaw­ień lub dodawanie nowych. Aby to zro­bić, wystar­czy dodać kole­jny plik .prop­er­ties (w swo­jej aplikacji powinieneś już mieć plik application.properties) doda­jąc do nazwy -nazwaPro­filu — w naszym przy­pad­ku będą to pli­ki application-development.properties oraz application-production.properties. Jeśli korzys­tasz z kon­fig­u­racji w for­ma­cie YAML, zasa­da jest taka sama — tworzymy nowy plik, doda­jąc do nazwy myśl­nik oraz nazwę pro­filu, w której ma obowiązywać.

Uwa­ga: Pli­ki te mogą być puste. Ich utworze­nie jest dobrym pomysłem, nawet jeśli póki co nie planu­jesz nad­pisy­wać w nich żad­nych ustaw­ień! Dzię­ki temu będzie jasne, że pro­fil o takiej nazwie ist­nieje i może być uży­ty w aplikacji.

Więcej o tym sposo­bie kon­fig­u­racji zna­jdziesz w dokumentacji

Aktywacja beanów

Dru­gi sposób pole­ga na warunk­owym uruchami­a­n­iu beanów — służy do tego adno­tac­ja @Profile(“profileName”). Adno­tację tą moż­na dodać nad klasą z adno­tacją @Component (lub dziedz­iczącą po niej, np. @Controller czy @Service) lub nad metodę z adno­tacją @Bean. 

Uwa­ga: adno­tac­ja ta może przyjąć wiele nazw pro­fili, bean będzie utwor­zony jeśli przy­na­jm­niej jeden z wymienionych pro­fili jest aktywny.

Szczegółowy opis adno­tacji @Profile i jej zas­tosowa­nia zna­jdziesz w doku­men­tacji Springa.

Podsumowanie

Pro­file są bard­zo elasty­cznym narzędziem, które pozwala znaczą­co uproś­cić kon­fig­u­rację aplikacji dla różnych środowisk. W przy­pad­ku pro­jek­tu bilet będziemy korzys­tali z 2 pro­fili — devel­op­ment oraz pro­duc­tion. W zależnoś­ci od pro­filu inna będzie kon­fig­u­rac­ja bazy danych oraz poziom logowania.

Łączymy naszą aplikację z bazą danych

W naszym kur­sie Javy imple­men­towal­iśmy już połącze­nie aplikacji Springowej z bazą danych — jeśli chcesz sobie odświeżyć pamięć, zajrzyj do lekcji 13. Opisy­wany tam sposób miał jed­nak kil­ka wad — po pier­wsze wyma­gał dostępu do dzi­ała­jącej na zewnątrz bazy danych za każdym razem, kiedy chcieliśmy uru­chomić aplikację. Dodatkowo nie mieliśmy kon­troli nad tym, co się w niej zna­j­du­je — potenc­jal­nie inna aplikac­ja mogła namieszać w naszej bazie danych powodu­jąc dzi­wne prob­le­my, mogliśmy zapom­nieć stworzyć tabelę itp. Ponieważ pro­jekt bilet ma być aplikacją ‘pro­duk­cyjną’, zaradz­imy temu z uży­ciem odpowied­nich bib­liotek i narzędzi.

Baza danych na żądanie — MariaDB4J

W przy­pad­ku bazy danych naszej aplikacji mamy dwa cele — po pier­wsze, aplikac­ja powin­na się uruchami­ać na dowol­nym środowisku bez dodatkowej kon­fig­u­racji w try­bie ‘testowym’ — tj. nie powodu­ją­cym zmi­an widocznych na innych instanc­jach aplikacji. Po drugie, dane dostępowe do bazy danych nie mogą być na repozy­to­ri­um (czyli nie może­my ich dodać np. do plików properties).

Dodatkowo, ponieważ planu­je­my uży­wać MySQL w ‘pro­duk­cyjnej’ insta­lacji, uży­wane przez nas rozwiązanie powin­no być zgodne.

Pier­wszy cel sto­sunkowo łat­wo może­my zre­al­i­zować korzys­ta­jąc z bazy danych w pamię­ci — HSQLDB czy H2 speł­ni­ały­by te kry­te­ria. Nieste­ty, skład­nia zapy­tań SQL jest nieco inna (szczegól­nie w przy­pad­ku tworzenia tabel) niż w przy­pad­ku MySQL. Ponieważ nie chce­my zdawać się nie JPA i mieć więk­szą kon­trolę nad struk­turą i dany­mi w bazie danych (o tym będzie kole­j­na sekc­ja), rozwiązanie to nie sprawdzi się w naszym projekcie.

Odpowiedz­ią było­by uruchami­an­ie MySQL z poziomu naszej aplikacji, w miarę możli­woś­ci bez koniecznoś­ci insta­lacji czegokol­wiek innego. MySQL dostar­czał takie rozwiązanie pod nazwą MySQL MXJ, nieste­ty ostat­nia wer­s­ja to 5.0.12 (rozwiązanie to nie jest kon­tyn­uowane). Na szczęś­cie ist­nieje też MariaDB4J — w zde­cy­dowanej więk­szoś­ci zgod­na z MySQL baza danych, którą ktoś przy­go­tował do uru­chomienia z poziomu JARa (nie wyma­ga więc insta­lacji niczego dodatkowego w sys­temie). Uwa­ga! Mari­aDB bazu­je na MySQL, ale jest rozwi­jana odd­ziel­nie — niek­tóre najnowsze funkc­je mogą nie być zgodne! Jeśli wyko­rzys­tu­jesz w pro­jek­cie bard­zo specy­ficzne funkc­je MySQL, sprawdź infor­ma­c­je o zgod­noś­ci. Minusem tego rozwiąza­nia jest rozmi­ar naszego pliku JAR — urośnie on dość znacznie. W więk­szoś­ci przy­pad­ków nie jest to prob­le­mem, ale warto o tym pamię­tać na wszel­ki wypadek.

Na potrze­by kur­su przyjmi­jmy, że do rozwi­ja­nia opro­gramowa­nia będziemy uży­wać pro­filu o nazwie devel­op­ment. Zakładamy, że skon­fig­urowałeś już środowisko na pod­staw­ie poprzed­niej sekcji tak, aby pro­fil ten był akty­wny jeśli uruchami­asz aplikację lokalnie. Potrze­bu­je­my jeszcze kon­fig­u­racji, która zami­ast głównej bazy danych naszej aplikacji poz­woli nam uru­chomić bazę testową, wypełnioną jakim­iś przykład­owy­mi dany­mi. Na początek doda­je­my zależność do naszego pro­jek­tu, doda­jąc do pliku pom.xml:

<dependency>
    <groupId>ch.vorburger.mariaDB4j</groupId>
    <artifactId>mariaDB4j</artifactId>
    <version>2.2.3</version>
</dependency>

Następ­nie doda­je­my kon­fig­u­rację Spring’a doda­jąc poniższą klasę:

@Configuration
@Profile("development")
public class DevelopmentDatabaseConfig {

  protected static final int DB_PORT = 3310;

  @Bean(name = "mariadb4j")
  public MariaDB4jSpringService mariaDB4j() {
    MariaDB4jSpringService service = new MariaDB4jSpringService();
    service.setDefaultPort(DB_PORT);
    return service;
  }
}

Możesz także zajrzeć na przykłady w doku­men­tacji MariaDB4J. Pozosta­je nam tylko poinstruowanie Springa o tym, jak ma się łączyć naszą z bazą danych, poprzez dodanie do pliku application-development.properties poniższych lin­i­jek (są to domyślne wartoś­ci, z jaki­mi uruchami­ana jest testowa baza danych):

spring.datasource.url=jdbc:mysql://localhost:3310/test
spring.datasource.username=root
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
spring.jpa.show-sql=true

Zwróć uwagę, że zmie­nil­iśmy domyśl­ny port (jest to 3306), aby nie koli­d­owało z ‘nor­mal­ną’ bazą danych, którą część pro­gramistów może mieć stale uru­chomioną. Po uru­chomie­niu aplikacji z uży­ciem pro­filu devel­op­ment, w kon­soli powin­naś zobaczyć logi MariaDB4J (będzie tam także wyjątek z Hiber­nate — ale o tym powiemy sobie za chwilę).

Konfiguracja produkcyjna

Dobrą prak­tyką jest nie umieszczanie infor­ma­cji dostępowych do pro­duk­cyjnej bazy danych na repozy­to­ri­um kodu, w głównym pliku .prop­er­ties moż­na albo użyć danych do środowiska QA albo nie uzu­peł­ni­ać ich wcale — aplikac­ja w takiej sytu­acji nie uru­cho­mi, jeśli nie akty­wu­je­my odpowied­niego pro­filu lub nie przekaże­my tych danych w inny sposób. Spring posi­a­da wiele mech­a­nizmów, dzię­ki którym może­my taki­mi ustaw­ieni­a­mi zarządzać poza naszą aplikacją — szczegóły rozważymy jed­nak w częś­ci o umieszcza­niu aplikacji na ser­w­erze i kon­fig­u­racji środowisk.

Przejmujemy kontrolę — Liquibase

Bazy danych to bard­zo potężne narzędzia, jed­nak jak ze wszys­tkim — możli­woś­ci są wprost pro­por­cjon­alne do iloś­ci wyma­ganej kon­fig­u­racji. W kur­sie Javy poz­wo­lil­iśmy JPA na samodzielne zarządzanie bazą danych, co było dla nas wystar­cza­jące. Było­by zapewne i w tej sytu­acji, ale zrób­my to poprawnie — prze­jmi­jmy stery!

O ile stan­dard SQL jest powszech­nie imple­men­towany przez różnego rodza­ju bazy danych, o tyle niek­tóre z nich rozsz­erza­ją go i udostęp­ni­a­ją jeszcze więcej specy­ficznych funkcji czy typów danych. Przykła­dem może być rozsz­erze­nie MySQL do danych przestrzen­nych ‑ponieważ nie jest to część stan­dar­d­owego SQL, JPA mógł­by mieć prob­lem z odpowied­nim dobraniem typów pól. Dodatkowym prob­le­mem mogą być dane początkowe — np. wstęp­na kon­fig­u­rac­ja, przykład­owe reko­rdy, pier­wszy użytkown­ik itp. W przy­pad­ku JPA imple­men­tac­ja takiej logi­ki i jej późniejsze utrzy­manie nie należą do najprost­szych, szczegól­nie w przy­pad­ku dużych skryptów.

Jed­nym z najpop­u­larniejszych rozwiązań tego prob­le­mu jest Liquibase — narzędzie, które pozwala na zarządzanie bazą danych (zarówno struk­turą jak i dany­mi) w sposób przy­ros­towy — kole­jne zmi­any są reprezen­towane przez np. kole­jne pli­ki, a narzędzie samo śledzi, które zmi­any zostały już zaap­likowane i w razie potrze­by nanosi pozostałe w ustalonej kole­jnoś­ci. Dzię­ki temu z repozy­to­ri­um znika­ją pli­ki SQL na kil­ka tysię­cy lin­i­jek, w których prześledze­nie intencji zmi­an graniczy z cud­em, a przy­pad­kowy błąd czy mody­fikac­ja nie w tym miejs­cu, co trze­ba, może się skończyć trag­icznie (dla naszych danych) w skutkach. Minusem jest oczy­wiś­cie konieczność stworzenia struk­tu­ry bazy danych ‘ręcznie’, poprzez odpowied­nio skon­struowane zapy­ta­nia SQL.

Liquibase moż­na uży­wać jako całkowicie osob­ne narzędzie, moż­na je zin­te­grować z Maven­em lub — w naszym przy­pad­ku najwygod­niejsze — użyć w połącze­niu ze Springiem.

Konfiguracja Liquibase ze Springiem

Pier­wszym krok­iem jest oczy­wiś­cie dodanie zależnoś­ci Mavenowej:

<dependency>
 <groupId>org.liquibase</groupId>
 <artifactId>liquibase-core</artifactId>
 <version>3.5.3</version>
</dependency>

W przy­pad­ku bib­liote­ki Liquibase, jest ona na tyle pop­u­lar­na, że Spring Boot ‘wie’ o konkret­nej wer­sji, może­my więc pom­inąć numer wer­sji w pliku pom.xml:

<dependency>
 <groupId>org.liquibase</groupId>
 <artifactId>liquibase-core</artifactId>
</dependency>

Następ­nie musimy określić, jak będziemy opisy­wać nasze zmi­any. Dostęp­nych opcji jest kil­ka, w tym XML, JSON, YAML czy SQL — my uży­je­my odrobiny XMLa i głównie SQL’a. W kat­a­logu z zasoba­mi umieszcza­my plik migration.xml o treści:

<databaseChangeLog
 xmlns="http://www.liquibase.org/xml/ns/dbchangelog/1.9"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog/1.9
 http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-1.9.xsd">
 <includeAll path="migration/"/>
</databaseChangeLog>

Jego jedyną rolą jest określe­nie, że definic­je zmi­an będziemy umieszczać w kat­a­logu migra­tion, i tam Liquibase będzie ich szukał. Doda­jmy więc nasze pier­wsze zmi­any — w tym wypad­ku tworzymy tabelę do prze­chowywa­nia danych logowa­nia użytkowników.

--liquibase formatted sql
--changeset author:jderda
CREATE TABLE `users` (
  `email` VARCHAR(254) NOT NULL PRIMARY KEY,
  `passwordHash` VARCHAR(60) NOT NULL,
  `token` VARCHAR(65) NULL,
  `active` BOOL NOT NULL DEFAULT 0,
  `deleted` BOOL NOT NULL DEFAULT 0
);

Pier­wsze dwie lin­ij­ki są komen­tarza­mi w skład­ni SQL, ale są wyma­gane w przy­pad­ku skład­ni SQL dla Liquibase — dzię­ki nim przekazu­je­my do Liquibase infor­ma­c­je o autorze oraz ewen­tu­alne dodatkowe metadane. Poza tym zapewne zas­tanaw­iasz się skąd dłu­gość pola ’email’ ma wartość 254? Tak się skła­da, że jest to maksy­mal­na dłu­gość adresu email, jej niety­powość jest wynikiem różnic w definicji określonych schematów pomiędzy różny­mi specy­fikac­ja­mi (więcej możesz znaleźć w tym świet­nym wątku na SO).

Ostat­nim krok­iem jest kon­fig­u­rac­ja Springa — może­my sko­rzys­tać z wspom­ni­anej wcześniej inte­gracji, ale może­my też użyć mech­a­niz­mu wbu­dowanego w Spring Boot.

Oczy­wiś­cie, wybier­amy opcję drugą, dlat­ego do pliku application.properties doda­je­my następu­jącą wartość:

liquibase.change-log:classpath:migration.xml

Alter­naty­wnie, może­my nasz plik changel­og zapisać w postaci YAML i umieś­cić go w domyśl­nej lokacji db/changelog/db.changelog-master.yaml.

Nieste­ty, w tym momen­cie napotkamy na pewien prob­lem — po uru­chomie­niu aplikacji, na kon­soli zobaczymy błąd i uru­chomie­nie nie powiedzie się. Prob­lem pole­ga na tym, że Spring nie gwaran­tu­je kole­jnoś­ci inic­jal­iza­cji beanów (domyśl­nie, może­my to na nim ‘wymusić’) — w tym wypad­ku, próbu­je stworzyć połącze­nie do bazy danych i migrację jeszcze przed fak­ty­cznym uru­chomie­niem naszej bazy danych. W więk­szoś­ci przy­pad­ków kole­jność, w jakiej dzi­ała Spring nei ma dla nas znaczenia, ale w tym przy­pad­ku złożyło się kil­ka czynników:

  • kwes­t­ia doty­czy bazy danych, która prze­ważnie dzi­ała w momen­cie uruchami­a­nia aplikacji
  • próbu­je­my uru­chomić migrację danych — pro­ces, który chce­my uru­chomić jak najszy­b­ciej (zan­im inne beany zostaną stworzone)
  • ręcznie’ uruchami­amy bazę danych już po uru­chomie­niu aplikacji

Rozwiązaniem tego prob­le­mu jest wymusze­nie na Springu określonej kole­jnoś­ci inicjowa­nia beanów — w naszym przy­pad­ku chce­my, aby bean MariaDB4jSpringService został uru­chomiony przed utworze­niem Data­Source. Najprost­szym rozwiązaniem jest sko­rzys­tanie z adno­tacji @DependsOn, pozwala­jącej określić jakie beany powin­ny zostać zainicjowane przed tym określonym beanem. Nieste­ty Data­Source jest twor­zony automaty­cznie przez Spring Boot, więc nie może­my w prosty sposób dodać rzec­zonej adno­tacji. Może­my jed­nak samodziel­nie utworzyć Data­Source doda­jąc odpowied­nią adno­tację (Spring jest na tyle spry­t­ny, że nie utworzy kole­jnego beana tego samego typu). Do naszej kon­fig­u­racji Devel­op­ment­Data­baseC­on­fig doda­jmy więc następu­jącą metodę:

@Bean
@DependsOn("mariadb4j")
public DataSource ds() {
  SimpleDriverDataSource ds = new SimpleDriverDataSource();
  ds.setDriverClass(Driver.class);
  ds.setUrl(String.format("jdbc:mysql://localhost:%d/test", DB_PORT));
  ds.setUsername("root");
  return ds;
}

Ta zmi­ana poz­woli bez prob­le­mu uru­chomić aplikację z uży­ciem pro­filu devel­op­ment. Uwa­ga! Uży­ty tutaj Sim­pleDriver­Data­Source nada­je się tylko i wyłącznie do testów i nie powinien być nigdy uży­wany w środowisku pro­duk­cyjnym — do tego sko­rzys­taj z automaty­cznej kon­fig­u­racji lub samodziel­nie skon­fig­u­ruj tzw. pulę połączeń. Warto zwró­cić także uwagę, że nasze ustaw­ienia doty­czące bazy danych w pliku application-development.properties nie są już potrzeb­ne. Dobrą prak­tyką jest jed­nak zostaw­ie­nie not­ki dla innych devel­op­erów, gdzie mogą szukać właś­ci­wej konfiguracji.

Po uru­chomie­niu aplikacji, w logach zna­jdziesz lin­ijkę podob­ną do tej:

2017-07-02 19:00:23.141  INFO 16865 --- [ost-startStop-1] liquibase : Successfully acquired change log lock
2017-07-02 19:00:24.308  INFO 16865 --- [ost-startStop-1] liquibase : Reading from test.DATABASECHANGELOG
2017-07-02 19:00:24.885  INFO 16865 --- [ost-startStop-1] liquibase : Successfully released change log lock

Oznacza to, że migrac­ja została przeprowad­zona (w tym wypad­ku tylko sprawd­zona — baza danych posi­a­da już wszys­tkie migrac­je) i nasza baza danych wyglą­da dokład­nie tak, jak tego oczekujemy!

Dane testowe / developerskie

Jed­ną z użytecznych, choć częs­to niedoce­ni­anych rzeczy są dane testowe. Uruchami­a­jąc aplikację lokalnie wygod­nym było­by, aby w aplikacji ist­ni­ał akty­wny użytkown­ik oraz były obec­ne jakieś reko­rdy aby moż­na było sprawdz­ić wido­ki czy API bez prze­chodzenia całej ścież­ki od początku. Jed­nocześnie bard­zo niefa­jnym było­by, gdy­by takie dane trafiły na pro­dukc­je — użytkown­ik “test” z hasłem “test” czy fik­cyjne zamówienia były źródłem kłopotów niejed­nej orga­ni­za­cji. Liquibase pozwala nam zaradz­ić temu prob­le­mowi poprzez mech­a­nizm zwany con­texts.

W dużym uproszcze­niu dzi­ała to podob­nie jak ‘pro­file’ w wer­sji dla bazy danych — wybrane skryp­ty migracji mogą być uruchami­ane tylko w określonym kon­tekś­cie — czyli np. w devel­op­er­skiej bazie danych, ale już nie w pro­duk­cyjnej. Może­my mieć także kil­ka kon­tek­stów i w ten sposób osob­no zarządzać dany­mi, które mają się znaleźć wszędzie (np. bazowa lista kat­e­gorii sklepu powin­na trafić także na pro­dukcję), tymi, które powin­ny być użyte na środowisku testowym/integracyjnym (np. konkret­na lista zamówień czy pro­duk­tów czy użytkown­ików) oraz tymi uży­wany­mi do devel­op­men­tu (dane każdego ‘typu’, ułatwia­jące testowanie na bieżąco).

Aby określić kon­tekst (lub kon­tek­sty), w jakim powinien uruchami­ać się dany skrypt, wystar­czy w pliku SQL użyć spec­jal­nej skład­ni, przykład­owo nagłówek:

--liquibase formatted sql
--changeset author:jderda

wystar­czy zmienić na następujący:

--liquibase formatted sql
--changeset author:jderda context:development

a następ­nie w naszym pliku application-development.properties dodać linijkę:

liquibase.contexts=development

I tutaj czy­cha pułap­ka, o której może­my się doczy­tać w jed­nym zda­niu doku­men­tacji (a dokład­niej “If you do not spec­i­fy a con­text when you run the migra­tor, ALL con­texts will be run.”). Aby uzyskać oczeki­wany przez nas efekt, w pliku application.properties dodaj linijkę:

liquibase.contexts=production

Te zmi­any wystar­czą, aby nasz plik SQL z adno­tacją context:development uru­chomił się tylko i wyłącznie, jeśli akty­wny jest Springowy pro­fil devel­op­ment (czyli w naszym przy­pad­ku, tylko na naszej testowej bazie danych).

Bardziej szczegółowa doku­men­tac­ja dostęp­na jest w ofic­jal­nym porad­niku Liquibase. Liquibase od wer­sji 3.3 ofer­u­je także podob­ne narzędzie o nazwie labels (różnice zostały opisane w poś­cie na blogu Liquibase), w naszym przy­pad­ku ograniczymy się jed­nak tylko do uży­cia kontekstów.

Podsumowanie

Liquibase nie jest koniecznoś­cią w wielu aplikac­jach — jeśli wystar­cza­jące jest tworze­nie i mody­fikowanie bazy danych przez JPA, a potenc­jal­ny koszt utraty danych (gdy np. zami­ast zmienić nazwę kolum­ny, usuniemy jed­ną i stworzymy inną w jej miejsce…) nie jest wyso­ki, Liquibase może okazać się bardziej stratą cza­su niż wartoś­cią. Najbardziej rozwiązanie to docenią pro­jek­ty, w których dane są bard­zo istotne, a baza danych uży­wana także przez np. anal­i­tyków lub inne systemy.

W pro­jek­cie bilet nie ma sil­nych przesłanek, aby zas­tosować tą tech­nologię, jed­nak w ramach nau­ki chcieliśmy pokazać co to jest i jak z niej korzys­tać. Alter­naty­wnym rozwiązaniem do Liquibase jest Fly­way — zasa­da dzi­ała­nia jest iden­ty­cz­na, różnią się jedynie deta­la­mi i sposobem kon­fig­u­racji (oczy­wiś­cie, w sieci możesz przeczy­tać dyskus­je oraz wraże­nia z uży­wa­nia każdej z nich — np. na Stack­Over­flow)

Strona pro­jek­tu Liquibase
Strona pro­jek­tu Flyway

Kod źródłowy

Przeglądaj kodPobierz ZIP

Kody źródłowe są dostęp­ne w ser­wisie GitHub — użyj przy­cisków po prawej aby pobrać lub prze­jrzeć kod do tego mod­ułu. Jeśli masz wąt­pli­woś­ci, jak posługi­wać się Git’em, instrukc­je i lin­ki zna­jdziesz w naszym wpisie na tem­at Git’a.

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przy­dat­ną, mamy małą prośbę: pol­ub nasz fan­page. Dzię­ki temu będziesz zawsze na bieżą­co z nowy­mi treś­ci­a­mi na blogu ( i oczy­wiś­cie, z nowy­mi częś­ci­a­mi kur­su Javy). Dzięki!