News Radar: AngularJS, Material Design i DB-agnostyczny Slick

Czas na kolejny update projektowy. Dzisiaj opowiem o webjars i wskażę kilka rzeczy na które należy zwrócić uwagę w momencie, kiedy chcemy korzystać z webjars w Play Framework. Wszystko to na przykładzie AngularJS i Angular-Material. Na koniec pokażę w jaki sposób napisać DAO Slickowe, które nie będzie miało bezpośrednich zależności do konkretnego silnika bazy danych.

Webjars

Ostatnio opisywałem m.in. w jaki sposób można dodać do projektu jQuery i Bootstrapa jako tzw. public assets w Play Framework. Już wtedy pisałem, że zastanawiam się nad wykorzystaniem webjars. Webjars to projekt w ramach którego, różnego rodzaju frontendowe technologie, (np. jQuery czy bootstrap) są pakowane w jar’y. Dzięki temu bardzo łatwo jest je dodawać do projektu jako np. dependency w sbt. Dzięki temu nie musimy (tak jak ja robiłem do tej pory) ręcznie ściągać plików i kopiować ich do odpowiednich katalogów. Zamiast tego deklarujemy dependency takie samo jakby to była zwykła biblioteka Javowa czy Scalowa. Dzięki temu np. zaktualizowanie Angulara do nowszej wersji jest tylko kwestią zmiany konfiguracji w sbt.

Lista dostępnych paczek znajduje się m.in. na stronie projektu http://www.webjars.org/

Przykładowo ja dodałem do projektu angulara i angular-material:

lazy val angularJs = "org.webjars" % "angularjs" % "1.6.2"
lazy val angularMaterial =  "org.webjars" % "angular-material" % "1.1.3"

Jak to jednak zwykle bywa, jeżeli załatwienie czegoś polega “tylko na…”, to zazwyczaj pojawi się po drodze mnóstwo kłopotów. W kolejnych sekcjach tego artykułu omówię na przykładzie tych dwóch bibliotek, które dołączyłem – na co trzeba zwrócić uwagę.

Wsparcie dla Angulara

Stwierdziłem, że średnio chce mi się klepać “surowe” ajax calle w jquery więc dodałem angulara do stacka technologi w News Radar.

Pierwszą rzeczą na którą chce zwrócić uwagę jest to, że (znowu…) zmieni nam się definicja routingu. Dla przypomnienia do tej pory routing dla public assetów wyglądał tak:

GET     /assets/*file           controllers.Assets.at(path="/public/lib/news-radar-gui", file)

Do tej pory było to spoko, ale ponieważ dodaliśmy zależność webjars to musimy trochę zmodyfikować ten wpis. Wszystko rozbija się o fakt, że webjar dodany do projektu będzie się znajdować w /public/lib/NAZWA_BIBLIOTEKI. Tak więc zmodyfikujmy lekko nasz routing:

GET     /assets/*file           controllers.Assets.at(path="/public/lib", file)

Jak łatwo się domyśleć, będziemy musieli zmienić również odwołania do plików js/css w projekcie. Przykładowo dla mojego skryptu w projekcie wygląda to tak:

<script type="text/javascript" src='@routes.Assets.at("news-radar-gui/scripts/feed.js")'></script>

A samego angulara tak:

<script type="text/javascript" src='@routes.Assets.at("angularjs/angular.js")'></script>

Druga sprawa związana z routingiem jest taka, że musimy też umieć skorzystać z routingu w kodzie javascript. W poprzednich update’ach pisałem w jaki sposób ogólnie dodać routing JavaScript – ale wcześniej cały kod js znajdował się w Play’owym szablonie strony. Tak więc mogliśmy użyć Playowych dyrektyw, żeby wskazać na metodę kontrolera. Teraz chciałbym, żeby był troszeczkę większy porządek w projekcie, więc wszystkie skrypty są w osobnych plikach.

Jak w takim razie dowiedzieć się na jaki adres powinien zostać wykonany ajaxowy strzał, żeby nasz Playowy kontroler zareagował? Jest to akurat dość proste. Dzięki temu, że zdefiniowaliśmy sobie JavaScript routing to po stronie przeglądarki mamy dostępny obiekt JavaScriptowy który reprezentuje nam adresy url metod kontrolera.

Przypominam – JavaScript router został zdefiniowany jako metoda o takiej postaci:

def feedJavascriptControllers = Action {
    implicit request => Ok(
      JavaScriptReverseRouter("jsRoutes")(
        controllers.routes.javascript.FeedListController.add,
        controllers.routes.javascript.FeedListController.load
      )
    ).as("text/javascript")
  }

W tym momencie jeśli chcielibyśmy mieć namiar na metodę load z FeedListController to z poziomu JavaScriptu pobierzemy sobie jej adres w ten sposób:

jsRoutes.controllers.FeedListController.add().url

Dużym ułatwieniem dla mnie było użycie Google Chrome i narzędzi developerskich podpiętych pod tą przeglądarkę. Z poziomu konsoli możemy sobie wywoływać metody z dostępnych nam aktualnie obiektów, więc łatwo (podpowiadanie nazw) możemy sprawdzić co się znajduje w naszym jsRoutes. Dzięki temu zobaczymy np. czy nie zapomnieliśmy wystawić jakieś metody z kontrolera.

Material Design w Angularze

Tak jak zastąpiłem jQuery Angularem, tak chciałem również zastąpić czymś bootstrapa, w którym troszeczkę trzeba się naklepać. Mój wybór padł na angular-material – pakiet który dodaje komponenty oparte o filozofię Material Design do projektu korzystającego z AngularJS. Do użycia angular-material wykorzystałem również webjarsy.

Tutaj tak naprawdę nie ma zbyt dużo problemów do rozwiązywania – a konkretnie nie przypominam sobie ani jednego problemu, który byłby spowodowany integracją z play frameworkiem, sbt, czy czymkolwiek. Ale skoro już i tak piszę o angular-material, to szybko wspomnę o jednej rzeczy na którą się naciąłem przerabiając tutorial do angular-material. Otórz chcąc skorzystać z angular-material trzeba go dodać do listy zależności naszego modułu angularowego:

var newsRadar = angular.module('newsRadar', ['ngMaterial']);

U mnie ta definicja (jeszcze bez modułu ngMaterial) znajdowała się w osobnym pliku. Czytając tutorial do angular-material dość bezmyślnie przekleiłem fragmenty kodu do siebie – m.in. i powyższą definicję. W tutorialu znajdowała się ona wprost w pliku html – więc i ja tam ją umieściłem, zapoimnając, że powinienem tylko zaktualizować wpis, który znajduje się w osobnym pliku. Przez to miałem dwie różne deklaracje tego samego modułu i kontrolki zdefniowane jako szablon komponentów angualara – przestały mi się renderować.

Agnostyczne do sterownika DAO w Slick

Pisząc swoje pierwsze DAO w Slicku trochę się krzywiłem, że mam bezpośrednie zależności do sterownika bazy H2. Wydawać by mi się mogło, że powinno istnieć ogólne API dostępu do bazy w które Slick wepnie konkretny sterownik bazy danych. Jednak w Slicku oddzielenie się od konkretnej bazy danych robi się troszeczkę inaczej.

W skrócie polega to na tym, że trzeba rozdzielić fazę tworzenia zapytania od fazy wywołania tego wywołania. W starym podejściu DAO miało po prostu zależność do obiektu Database (które należy do implementacji H2) – obiekt ten służy do odpalania zapytań, więc mogliśmy w tym samym momencie stworzyć zapytanie i odpalić je:

override def load: Seq[Feed] = Await.result(db.run(table.result), Duration.Inf)

Wiem, że to podejście jest trochę słabe, bo nie wykorzustyje faktu, że Slick z defincji operuje na bazie w sposób asynchroniczny z naszego punktu widzenia – ale tą kwestię poprawię w przyszłości.

Teraz DAO nie będzie korzystać z obiektu Database ale z JdbcProfile. Nie będziemy mogli jednak już odpalać zapytania na bazie – będziemy mogli je tylko przygotować:

abstract class FeedDbDao(val driver: JdbcProfile) extends FeedDao {

  import driver.api._

  class Feeds(tag: Tag) extends Table[Feed](tag, "FEEDS") {
    def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
    def name = column[String]("NAME")
    def address = column[String]("ADDRESS")
    def * = (id, name, address) <> (Feed.tupled, Feed.unapply)
  }

  protected lazy val table = TableQuery[Feeds]

  protected def loadDBIO: DBIO[Seq[Feed]] = table.result

  protected def storeDBIO(feed: Feed): DBIO[Long] = table returning table.map(_.id) += feed
}

Ta klasa nie ma zależności do konkretnej implementacji bazy danych. Koniec końców jednak chcielibyśmy móc te zapytania wywołać. Stworzyłem więc drugą klasę, która nie robi praktycznie nic ponad odpalanie zapytań przygotowanych w FeedbackDbDao. Klasa ta na ten moment znajduje się w osobnym module sbt – tak więc podmiana implementacji bazy danych to kwestia wymiany zależności w build.sbt oraz zmiana konfiguracji injectora w Guice. Klasa oparta o H2 ta wygląda tak:

class FeedH2Dao @Inject() (db: Database) extends FeedDbDao(H2Driver) {
    Await.result(
      db.run(MTable.getTables).flatMap(
        v => {
          val names = v.map(m => m.name.name)
          val create = Seq(table).filter(
            t => !names.contains(t.baseTableRow.tableName)
          ).map(_.schema.create)
          db.run(DBIO.sequence(create))
        }
      ), Duration.Inf)

  def store(feed: Feed) : Long = {
    Await.result(
      db.run(storeDBIO(feed)),
      Duration.Inf
    )
  }

  def load: Seq[Feed] = {
    Await.result(
      db.run(loadDBIO),
      Duration.Inf
    )
  }
}

Jak widać również do tej klasy przeniesiony został kod automatycznego tworzenia schemy, ale w zasadzie nie jest to nic dziwnego, biorąc pod uwagę, że tylko ta klasa “wie” jak odpalić query na bazie.

Podsumowanie

Jak widać kolejny tydzień w zasadzie zajmuję się tylko technikaliami projektu, ale kompletnie nic nie posuwam go do przodu z punktu widzenia samych funkcjonalności. Co prawda z góry założeniem tego projektu było, żeby skorzystać z dużej ilości różnych technologii z których dotychczas nie korzystałem, ale mówiąc szczerze już mnie troszeczkę znudziło odpalanie aplikacji jednym formularzem i tabelką ;) Dlatego też myślę, że na razie wystarczy i teraz przez jakiś czas skupię się nad tym, żeby zbliżyć się do stworzenia jakiegoś sensowego MVP.