Kotlin, Spring Boot a Heroku – Databáza (3/4)

Predošlá časť - Vytvorenie API Nasledujúca časť - Heroku

Niekde v internete bude naša databáza, do ktorej budeme ukladať naše dáta. Alebo ich z nej aj načítavať. Databáza má tabuľky. Kedysi sa ešte reálne písali SQL dotazy na získanie týchto dát, ale dnes už na to používame ORM – jednoducho objekty v spojení s niečim, čo ich vie transformovať na SQL príkazy na pozadí (bez toho, aby sme o tom museli nejak extra veľa vedieť).

Poznámka: Okej, nie vždy sa využíva ORM. Má aj svoje nevýhody. Napr. môže byť trochu pomalšie ako SQL príkazy.

Spring Boot na ORM má Spring Data JPA (Java Persistance API) a Hibernate. Na to, aby sme ich mohli využívať musíme najprv pridať dependency (package, libku, akokoľvek to chceme volať) do Gradlu – upravením súboru build.gradle. Pod riadok implementation("org.springframework.boot:spring-boot-starter-web") vložme:

implementation("org.springframework.boot:spring-boot-starter-data-jpa")

Poznámka: Pokiaľ riadok pridáme do dependencies, tak nezáleží, na ktoré miesto ho dáme. Pokiaľ viem, tak na poradí dependencies nezáleží. Daj len pozor, aby si to pridal/a do dependencies bloku.

Zároveň budeme chcieť náš kód spúšťať aj bez toho, aby sme si museli nejaku databázu niekde vytvárať (aspoň zatiaľ). Na to si pridáme ďalšiu dependency:

implementation("com.h2database:h2")

Týmto pridáme závislosť na H2 databázu, ktorá vie pracovať in-memory alebo z lokálneho súboru. To je užitočné hlavne na testovanie, aby sme sa nemuseli stále pripájať niekam ďaleko (a hlavne, aby sme databázu zatiaľ nemuseli setupovať).

Ak používaš IDEA, tak potrebuješ “syncnúť” Gradle (napravo hore v editore by si mal/a vidieť ikonku sloníka s refresh šípkami – to je ono! Klikni na to!). Ak si v inom editore, tak ti to netreba.

Ako syncnúť Gradle v IDEA
Ako syncnúť Gradle v IDEA

Konfigurácia databázy

Pred tým ako prejdeme na všetko kódenie si ešte musíme nakonfigurovať, ako naša appka bude komunikovať s databázou. Takéto veci sa konfigurujú v súbore src/main/resources/application.properties (Čo to vlastne sú tie application.properties?). Spring má preddefinované nejaké properties, ktoré na pripojenie k databáze očakáva a pri štarte aplikácie ich načítava. Náš application.properties súbor bude vyzerať takto:

spring.datasource.url = jdbc:h2:file:./data/passwords
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=Password123+
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update

Podľa názvu sa dá celkom pekne odvodiť, čo asi robia, ale poďme si niektoré (alebo všetky) aj tak prejsť. spring.datasource.url Spring-u hovorí, kde našu databázu nájde. Na testovanie chceme využívať lokálnu databázu, ktorá bude uložená v zložke data/passwords. spring.datasource.driverClassName je názov driveru, ktorý chceme pre databázu používať. Driver je proste niečo, pomocou čoho pristupujeme k databáze – netreba to príliš riešiť. Väčšina známych databáz má svoj driver (MySQL driver, PostgreSQL driver, …).

Tento driver sa nachádza práve v H2 dependency (com.h2database:h2), ktorú sme pridali vyššie. Podobným spôsobom neskôr pridáme a nakonfigurujeme PostgreSQL driver. spring.datasource.username a spring.datasource.passwordsú asi jasné. spring.jpa.database-platform súvisi opäť s výberom databázy – neskôr tam budeme mať PostgreSQL. spring.jpa.hibernate.ddl-auto=update nám zabezpečí, že Spring nám databázu automaticky vytvorí. What?

Yup, Spring Boot nám celú databázu automaticky vytvorí podľa našich modelov. Hneď sa k tomu dostaneme.

Ako to s tou databázou teda funguje?

Najprv rýchly prehľad a potom prejdeme k reálnemu kódeniu (alebo ku konfigurácii).

V minulosti by to bolo hlavne o kódení. Teraz nám stačí málo kódu a trošku konfigurácie (niekedy rozmýšľam, či som programátor, alebo skôr konfigurátor). Spring Boot nám vie sám vytvoriť celú databázu aj s tabuľkami. Ako? Tak, že mu musíme povedať, ktoré classy má pretransformovať na tabuľky. Tieto classy sa voľajú modely.

Keď už má tie tabuľky a vie, ktoré modely (classy) k ním prislúchajú, tak potrebujeme niečo, čo nám bude vedieť modely ukladať do databázy – tu prichádza na scénu Repository. Repository je generický interface, ktorý obsahuje metódky na základné CRUD operácie nad nejakou triedou. Je sprostredkovaná Spring-om.

To znamená, že keď si vytvoríme Repository<PasswordModel>, tak budeme vedieť do databázy vkladať PasswordModel-y, budeme ich vedieť získavať a tak ďalej. A to všetko bez toho, aby sme písali nejak veľa kódu. Mega vec.

Takže už máme Controller, budeme mať Modela Repository. Ešte nám k tomu chýba jedna vec, ktorá nám prepojí Controller s Repository. Prečo nemôžeme prepojiť Controller priamo s Repository? Mohli by sme, ale chceme sa naučiť písať udržiavateľný kód a tak chceme mať veci pekne oddelené. Toto prepojenie medzi Controller-om a Repository si nazveme Service.

Mimochodom, všetky názvy, ktoré používam nie sú vymyslené mnou, ale sú zaužívané v Spring Boot-e, takže sa s nimi stretneš skoro v každom projekte. Podobné (ak nie rovnaké) názvy sú zaužívané aj v ASP.NET v C# a všeobecne v MVC API frameworkoch.

Poďme si všetky tieto classy teda povytvárať.

PasswordModel

Model všeobecne označuje triedy, ktoré nám definujú nejaké dáta. Náš model bude vyzerať takto:

package sk.streetofcode.passwords

class PasswordModel(var username: String, var password: String, var url: String)

Krásne. Žiadne Javovské gettery a settery. Kotlin je top. Máme náš model. Spring Boot ešte ale nevie, že z neho má robiť tabuľky. Ako mu to povieme? Anotácie FTW. Konkrétne použijeme @Entity anotáciu:

package sk.streetofcode.passwords

import javax.persistence.Entity

@Entity
class PasswordModel(var username: String, var password: String, var url: String)

Názov tabuľky, ktorú ma Spring vytvoriť zadefinujeme pomocou ďalšej anotácie – @Table:

package sk.streetofcode.passwords

import javax.persistence.Entity
import javax.persistence.Table

@Entity
@Table(name = "passwords")
class PasswordModel(var username: String, var password: String, var url: String)

Keďže už z toho budeme mať databázovú tabuľku, tak by sme mali pre naše heslá mať aj nejaké ID. Upravme našu triedu na:

package sk.streetofcode.passwords

import javax.persistence.*

@Entity
@Table(name = "passwords")
class PasswordModel(
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  var id: Long? = null,

  var username: String,
  var password: String,
  var url: String
) {
  constructor() : this(null, "", "", "")
}

Trochu som upravil aj formátovanie, nech sa na to lepšie pozerá. @Id anotácia springu povie, že tento atribút je ID a @GeneratedValue anotácia mu povie, akým spôsobom sa táto hodnota bude generovať. AUTO znamená, že sa prispôsobí použitej databáze. V našom prípade bude generovať IDčka od 1 postupne vyššie. id je null-ovateľné, aby sme ho nemuseli nastavovať pri vytvárani objektov tejto triedy – id sa nastaví pri vkladaní do databázy. PasswordModel musí mať aj defaultný konštruktor (bez parametrov), aby Spring vedel objekt vytvoriť (teoreticky sa dá použiť aj tento gradle plugin, ale prišlo mi jednoduchšie tam proste dať ten konštruktor).

To je všetko. Máme model.

PasswordRepository

Ako som už povedal, Repository potrebujeme na to, aby sme vedeli model jednoducho ukladať do/získavať z databázy. Nejdem to naťahovať a rovno ukážem celú našu repozitory:

package sk.streetofcode.passwords

import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository

@Repository
interface PasswordRepository: CrudRepository<PasswordModel, Long> {
}

@Repository anotácia povie Spring-u, že táto classa je repository. Dedíme od CrudRepository classy, ktorá obsahuje metódy na všetky CRUD operácie. Prvý generický typ definuje model, ktorý budeme upravovať (náš PasswordModel) a druhý generický typ (Long) definuje typ ID-čka daného modelu. Simple enough. Už to skoro máme.

PasswordService

Už iba to prepojenie medzi Controller-om a Repository. Táto trieda bude mať anotáciu @Service. V skratke to znamená, že daná trieda sa bude dať injectnúť do Controller-a (a vôbec hocikam). Detaily teraz neriešme. Okrem toho, že táto trieda sa bude dať injectnút, budeme aj DO tejto triedy injectovať. A to konkrétne našu PasswordRepository. Na začiatku bude teda náš Service vyzerať takto:

@Service
class PasswordService(var passwordRepository: PasswordRepository)

V našom kóde PasswordService nikdy nebudeme vytvárať (volať konštruktor). Budeme ho iba injectovať do nášho Controller-u. Spring je taký inteligentný, že pri injectovaní si všimne, že PasswordService zas potrebuje PasswordRepository a tak mu tam aj tú injectne. Magic.

Poznámka: Dependency injection je koncept, ktorý využiješ nie len pri API projektoch, tak sa oplatí si o tom niečo naštudovať. Väčšina webových frameworkov má nejakým spôsobom DI implementovanú. Prípadne ešte odporúčam prečítať si niečo o IoC – inversion of control.

Okej, náš Service ale zatiaľ nič nerobí. Pridajme mu metódy na získanie všetkých PasswordModel-ov a na pridanie nového PasswordModel-u:

package sk.streetofcode.passwords

import org.springframework.stereotype.Service

@Service
class PasswordService(var passwordRepository: PasswordRepository) {
  fun getAll(): List<PasswordModel> {
    return passwordRepository.findAll().toList()
  }

  fun add(username: String, password: String, url: String): PasswordModel {
    val passwordModel = PasswordModel(null, username, password, url)
    return passwordRepository.save(passwordModel)
  }
}

Trochu si našu triedu skotlinujme:

package sk.streetofcode.passwords

import org.springframework.stereotype.Service

@Service
class PasswordService(var passwordRepository: PasswordRepository) {
  fun getAll() = passwordRepository.findAll().toList()

  fun add(username: String, password: String, url: String) =
    passwordRepository.save(PasswordModel(null, username, password, url))
}

Cool. Funkcia getAll vracia List všetkých PasswordModel-ov, ktoré máme v databáze. Funkcia add nám vráti model, ktorý vytvorila a uložila do databázy. Taká je konvencia. add funkciu by som za normálnych okolností napísal asi normálne so zátvorkami a return-om, ale tak prečo neukázať, čo Kotlin dokáže? (aj tak ju neskôr budeme musieť rozpísať)

PasswordController

Teraz máme konečne všetko pripravené na to, aby sme iba upravili náš Controller a aby sme mohli získať veci z našej databázy. Do nášho Controller-a musíme injectnúť PasswordService – na to nám stačí ho pridať ako property tejto triedy – Spring si domyslí. A už iba upravíme našu get metódku, aby vrátila dáta z PasswordService-u (opäť použijeme kotlinovskú skrátenú syntax):

package sk.streetofcode.passwords

import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/passwords")
class PasswordController(
  var passwordService: PasswordService,
  private val passwords: MutableList<String> = mutableListOf()
) {
  @GetMapping("")
  fun get() = passwordService.getAll()

  @PostMapping("")
  fun post(@RequestBody password: String) {
    passwords.add(password)
  }
}

Zároveň upravme aj našu post metódu na uloženie nového hesla:

package sk.streetofcode.passwords

import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/passwords")
class PasswordController(
  var passwordService: PasswordService,
  private val passwords: MutableList<String> = mutableListOf()
) {
  @GetMapping("")
  fun get() = passwordService.getAll()

  @PostMapping("")
  fun post(username: String, password: String, url: String) = 
    passwordService.add(username, password, url)
}

Hm. Spring ale nevie, odkiaľ má zobrať parametre našej add funkcie. Mohli by sme napríklad pred každý parameter pridať @RequestParam anotáciu a vtedy by sme request volali nasledovne:

POST http://localhost:8080/password/add?username=habla&password=bubla&url=hablabubla

Ale keďže robíme POST request, tak je zaužívaná konvencia, že naše parametre pošleme ako JSON v tele (body) requestu. What? Neriešme detaily, pozrime sa radšej, ako to v Spring-u spraviť.

Najprv si vytvoríme objekt, ktorý sa bude v tele requestu posielať (ja som si ho dal do samostatného súboru):

package sk.streetofcode.passwords

data class AddPasswordRequest(
  val username: String,
  val password: String,
  val url: String
)

A teraz môžeme dokončiť náš PasswordController:

package sk.streetofcode.passwords

import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/passwords")
class PasswordController(
  var passwordService: PasswordService,
  private val passwords: MutableList<String> = mutableListOf()
) {
  @GetMapping("")
  fun get() = passwordService.getAll()

  @PostMapping("")
  fun add(@RequestBody request: AddPasswordRequest) =
    passwordService.add(request)
}

Žial, musíme upraviť aj našu PasswordService.add metódu, aby brala AddPasswordRequest ako parameter:

package sk.streetofcode.passwords

import org.springframework.stereotype.Service

@Service
class PasswordService(var passwordRepository: PasswordRepository) {
    fun getAll() = passwordRepository.findAll().toList()

    fun add(request: AddPasswordRequest): PasswordModel {
        val (username, password, url) = request
        return passwordRepository.save(PasswordModel(null, username, password, url))
    }
}

Spring teraz vie, že keď mu príde POST request na /passwords, tak má očakávať v tele do JSON-u zoserializovaný objekt AddPasswordRequest. Keď tento request príde, tak sa ho Spring pokúsi deserializovať, aby sme my už dostali objekt.

Poznámka: JSON je predvolený formát, takže nemusíme nič nastavovať.

Príde ti divný tento riadok – val (username, password, url) = request? Nebudem zbytočne opisovať čo to znamená, keď už je to pekne opísané v dokumentácii.

Ouk. Aplikáciu si môžme spustiť a otestovať. Keď sa nám aplikácia spustila, navigujme sa v browseri (alebo zavolajme request z Postman-a) na: http://localhost:8080/password

Mali by sme dostať prázdny JSON (prázdne pole). To preto, že sme opäť zatiaľ nepridali žiadne heslo. Skúsme ho teda pridať cez Postman-a pomocou nášho POST /passwords endpointu:

POST na /passwords
POST na /passwords

 

Keď opäť skúsime pozrieť na http://localhost:8080/password, tak si všimeneme, že už tam nejaké to heslo máme. Magic.

Screenshot výsledku GET requestu
GET /passwords

Tým, že H2 databázu máme nakonfigurovanú do súboru, tak by sa nám mali naše heslá uchovávať aj cez reštarty aplikácie. Keby používame in-memory variantu H2 databázy, tak sa databáza pri vypnutí aplikácie proste stratí, pretože sa vymaže všetka pamäť (RAM) nášho procesu.

Oukej, tým by sme mali vytvorenú našu základnú appku, ktorú sa následne pokúsime deploynúť do Heroku spolu s databázou.

Kód z tejto časti tutoriálu nájdeš na našom GitHub-e.

Predošlá časť - Vytvorenie API Nasledujúca časť - Heroku


Pridaj komentár

Vaša e-mailová adresa nebude zverejnená.