Scala backend CRUD exemplu

Am nevoie de o confirmare ca am ales o varianta buna pentru structurarea unui api in Scala. Ca si exemplu voi folosi o metoda get:

Controller:

def get(country: String) = Action.async {
    SettingsDataRowsService.getSettings(country) match {
      case Success(value) => Future.successful(Ok(Json.toJson(value)))
      case Failure(_) => Future(InternalServerError)
    }
  }

Serviciu:

def getSettings(country: String): Try[List[SettingsDataRow]] = {
    try {
      var result = SettingsDataRowsRepo.getFiltered(country).get
      ... cateva procesari
      Success(result)
    }
    catch {
      case e: Exception => Failure(e)
    }
  }

Repo:

def getFiltered(country: String): Try[List[SettingsDataRow]] = {
  val sqlQuery = "query_string"
  val conn: AppConnection = new AppConnection()
  val stmt: Statement = conn.getStmt()

  var list = new ListBuffer[SettingsDataRow]()
try {
      val queryResult = stmt.executeQuery(sqlQuery)

      while (queryResult.next()) {
        settingsList += new SettingsDataRow(
          None,
          queryResult.getString("col1"),
          queryResult.getString("col2"),
          queryResult.getBigDecimal("col3"),
          queryResult.getInt("col4"),
          queryResult.getBigDecimal("col5")
        )
      }

      Success(list.toList)
    }
    catch {
      case se: SQLException => {
        se.printStackTrace()
        Failure(se)
      }
      case e: Exception => {
        e.printStackTrace()
        Failure(e)
      }
    } finally {
      conn.closeStmt()
      conn.closeConnection()
    }
}

In Controller am lasat strict apelarea serviciului si cazurile ce pot fi tipurile de raspuns, atat as vrea sa contina pentru fiecare metoda.
In Serviciu am mutat toate procesarile de date, utilizand try catch’uri si returnand intotdeauna un Try (metodele care nu returnau nimic le-am fortat sa returneze un Try[Boolean]). De aici apelez repo’ul (ce contine interogari din db) si “pregatesc/procesez” raspunsul final pentru controller.
In Repository incerc sa tin doar metode ce fac operatii pe baza de date, implicit compunerea queryurilor (din serviciu trimit datele deja formate pentru queryuri)

Astfel, Serviciul este legatura dintre Controller si Repo, fiecare din cele 3 cu logica sa. Este o abordare corecta? Ceva asemanator foloseam si in C#, deci limbajul nu este atat de important cat structura.

Specific Scala … am renuntat sa folosesc try catch’uri in Controllere pentru a evita excesul, folosirea lor in cam toate metodele aplicatiei. Cu try catch, controllerul ar fi aratat astfel (si eram nevoit sa folosesc .get pentru a scoate raspunsul din Try):

try {
       val response = SettingsDataRowsService.getSettings(country).get
       Future.successful(Ok(Json.toJson(response)))
     } catch {
       case e: Exception =>
         e.printStackTrace()
         Future.successful(InternalServerError)
    }

https://tools.ietf.org/html/rfc7231

Mie arhitectura imi pare ok, dar in controller as returna Standard HTTP Status Codes care sa reflecte adevarul despre resursa respectiva. De exemplu in codul de mai sus( in controller) as returna 404 Not Found atunci cand getSettings() din serviciu nu returneaza rezultate.

multumesc. Pai m-am gandit ca getSettings() returneaza o lista, deci chiar daca din db nu vin elemente, tot va returna un array gol. Pe langa asta, in cazul de fata, este obligatoriu ca minim 7 elemente sa existe, daca nu, sunt create si acestea vor fi returnate. Deci 404 nu e un caz in situatia data. Sigur m-as incurca putin acolo unde as avea un getById, si ar trebui sa returnez ori Ok(200), ori NotFound(404), ori InternalServerError(500), pentru ca voi avea 3 cazuri si ar trebui sa schimb metoda din controller, nu pot avea mai multe cazuri de Success sau Failure. Sau da? naiba stie :))) trebuie sa ma mai joc putin cu cazurile astea. netestat …

Serviciu:

def getById: Try[Option[SettingsDataRow]] = {
try {
val setting = new SettingsDataRow(None, “”, “”, 0, 0, 0)
if (setting == None) {
Success(None)
}
Try(Option(setting))
}
catch {
case e: Exception => Failure(e)
}
}

Controller

def getById = Action.async {
SettingsDataRowsService.getById match {
case Success(value) => Future.successful(Ok(Json.toJson(value.get)))
case Success(none) => Future.successful(NotFound(“Not found”))
case Failure(_) => Future(InternalServerError)
}
}

s-ar putea sa nu fie functional, intr-un caz serviciul returneaza un Option, in altul un Success, iar in controller am 2x Success :smiley:

Try[Option[SettingsDataRow]] => sa pot returna 404 “personalizat” (sau status codes din controller), sunt obligat sa folosesc sa folosesc mereu Option[Entitate] si sa fac verificarea (if entity == None) return Success(None), altfel se va duce pe cazul catch Exception care va returna un Failure cu status 404. Cu cat aflu mai multe, imi dau seama ca nu exista arhitectura perfecta, exista multe variante, fiecare cu avantajele si dezavantajele ei, si poate fi “si o chestie de gust”.

In paradigma REST un endpoint poate sa returneze mai multe raspunsuri( HTTP Status Codes). Clientul care foloseste API-ul trebuie sa actioneze conform cu raspunsul endpoint-ului respectiv si poate face asta fie interpretand HTTP status code-ul fie evauland un mesaj custom returnat de catre acel endpoint.

2 Likes

In cazul exemplului de mai sus am renuntat la Try in favoarea Option pentru a returna toate tipurile de StatusCode disponibile pentru aceasta metoda: 200, 404 sau 500.

Serviciul:

def getById(): Option[SettingsDataRow] = {
try {
val testSetting = new SettingsDataRow(None, “”, “”, 0, 0, 0) // simulare get din db

  if (testSetting == None) {
    None
  }

  Option(testSetting)
} catch {
  case e: Exception => throw new Exception(e)
}

}

Controllerul:

def getById() = Action.async {
Future {
      settingsService.getById() match {
        case Some(value) => Ok(Json.toJson(value))
        case None => NotFound
        case _ => InternalServerError("Internal Server Error")
      }
    }
}

momentan, o metoda mai buna de atat de a acoperi toate cazurile nu gasesc. Inca ma gandesc daca e rolul metodei din controller sa formateze data de tip JSON (return Ok(Json.toJson(value))) sau sa imping operatia asta mai in spate, in serviciu. In cazul in care tipul returnat variaza, JSON/XML sa nu stau sa pun tot felul de conditii in controller. Dar daca duc asta in servicii, inseamna ca toate acestea vor returna JsResult/JsError si nu T sau Option[T] sau orice altceva.

edit: mi-am dat seama ca am spus o prostie :))) nu pot returna JsResult/JsError din servicii, in ideea de a avea mai multe tipuri de raspuns, altfel XML n-ar mai fi valabil. Trebuie tot ceva generic

In clasele de servicii as returna strict datele in structura lor. Uneori e nevoie sa folosesti datele returnate de un serviciu in mai multe controllere sau chiar in alte servicii. Ar fi ciudat sa trebuieasca sa convertesti din JsResult.

Poti sa incerci si o abordare “Exceptions driven architecture” in care poti sa creezi exceptii custom pe care le arunci din diverse servicii in functie de logica de business. Exceptiile le interceptezi in metoda din controller si in functie de tipul de exceptie( si de mesajul din exceptie) returnezi un HTTP Status code potrivit.

Abordarea asta este oreacum similara cu instructiunile GO TO, dar exista si dezavantaje pentru ca in unele limbaje exceptiile afecteaza performanta.

1 Like