ScalaAnorm - opensas/Play20Es GitHub Wiki

Anorm, accediendo fácilmente a datos SQL

Play incluye una simple capa de acceso a base de datos llamada Anorm que utiliza sentencias SQL para interactuar con la base y provee una API para parsear y transformar los datos obtenidos de la base.

Anorm NO es un ORM (Object Relational Mapper)

En la siguiente documentación usaremos la base de datos de ejemplo de MySQL.

Si desea utilizarla para su aplicación, siga las instrucciones del sitio de MySQL, y habilítela para su aplicación agregando la siguiente línea de configuración al archivo conf/application.conf:

db.default.driver= com.mysql.jdbc.Driver
db.default.url="jdbc:mysql://localhost/world"
db.default.user=root
db.default.password=secret

Introducción

Hoy en día, puede resultar extraño volver a las viejas sentencias SQL para acceder a una base de datos SQL, en especial para los desarrolladores Java acostumbrados a utilizar ORMs de alto nivel como Hibernate para olvidarse por completo de SQL.

Aún cuando estamos de acuerdo en que este tipo de herramientas son casi obligatorias en Java, pensamos que no son necesarias en absoluto cuando contamos con un lenguage de programación de alto nivel como Scala. Por el contrario, no tardarán mucho en volverse contraproducentes.

Usar JDBC es doloroso, pero nosotros le damos una API más placentera

Usar la API de JDBC de manera directa es un proceso tedioso, en especial en Java. Hay que lidiar con checked exceptions por todos lados, e iterar una y otra vez los ResultSets para transformar la información en crudo a nuestras propias estructuras de datos.

Nosotros brindamos un API mucho más simple para JDBC, usando Scala no necesita preocuparse por las exceptions, y transformar los datos es realmente fácil con un lenguaje funcional. De hecho, el objetivo de la capa de datos de Play para Scala es proveer varias APIs que le permitan transformar de manera simple la información obtenida mediante JDBC a sus propias estructuras de datos.

No necesita otro DSL para acceder a una base de datos relacional

SQL es el mejor DSL (lenguaje de dominio específico) que existe para acceder a bases de datos relacionales. No necesitamos inventar nada nuevo. Es más, la sintaxis de SQL y las prestaciones pueden diferir de una base de datos a otra.

Si intenta abstraerse de esto con otro DSL similar a SQL, tendrá que lidiar con diversos ‘dialectos’ específicos para cada base de datos (como lo hace Hibernate), y evitar usar las prestaciones propias y exclusivas de una base de datos.

En ocasiones Play le brindará sentencias SQL pre-cargadas de datos, pero la idea es no ocultar el hecho de que por debajo estamos usando SQL. Play simplemente le evita tipear un montón de caracteres para hacer una simple consulta, y siempre podrá recurrir a escribir directamente la sentencia SQL de manera manual.

Un DSL 'tipado' para generar SQL es un error

Algunos argumentan que un DSL con seguridad de tipos es mejor, ya que todas sus consultas son verificadas por el compilador. Sin embargo, el compilador verifica sus consultas basándose en la definición de meta-datos que a menudo la escribe ustede mismo al 'mapear' su estructura de datos con el esquema de la base de datos.

Nada le garantiza que esta meta-información sea correcta. Incluso cuando el compilador asegure que su código y sus consultas tienen los tipos de datos adecuados, aún así podrá fallar miserablemente en tiempo de ejecución debido a que el esquema real de la base de datos no coincide con la meta-información.

Tome el control de su código SQL

Los ORM (Mapeadores objeto-relacionales) funcionan bien para casos triviales, pero cuando tiene que trabajar con un esquema complejo de datos, o con bases de datos heredadas, se pasará la mayor parte del tiempo peleando con el ORM para que produzca las consultas SQL que usted precisa.

Escribir las sentencias SQL a mano puede ser tedioso para una simple aplicación ‘Hola Mundo’, pero para cualquier aplicación real, ahorrará tiempo y simplificará su código tomando el control completo de su código SQL.

Ejecutando consultas SQL

Antes que nada debe aprender cómo ejecutar consultas SQL.

Primero, importe anorm._, y luego simplemente utilice el objeto SQL para creat consultas. Precisará un objeto Connection para ejecutar una consulta, que puede obtener a partir de la utilidad play.api.db.DB:

import anorm._ 

DB.withConnection { implicit c =>
  val result: Boolean = SQL("Select 1").execute()    
} 

El método execute() retorna un objeto de tipo Boolean que indica si la ejecución fue exitosa.

Para ejecutar un update, puede utilizar el método executeUpdate(), que retorna la cantidad de filas actualizadas.

val result: Int = SQL("delete from City where id = 99").executeUpdate()

Dado que Scala soporta strings de múltiples líneas, puede utilizarla para escribir sentencias SQL complejas:

val sqlQuery = SQL(
  """
    select * from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where c.code = 'FRA';
  """
)

Si su consulta SQL precisa parámetros dinámicos, puede declarar variables como {name} en la consulta, para luego asignarles un valor:

SQL(
  """
    select * from Country c 
    join CountryLanguage l on l.CountryCode = c.Code 
    where c.code = {countryCode};
  """
).on("countryCode" -> "FRA")

Obteniendo datos utilizando la API Stream

La primera manera de acceder a los resultados de una sentencia select es utilizando la API Stream.

Cuando llama al método apply() en una sentencia SQL, recibirá un Stream lazy de instancias de Row, en la cual cada fila puede ser vista como un diccionario:

// Creamos una consulta SQL
val selectCountries = SQL("Select * from Country")
 
// Transform the resulting Stream[Row] as a List[(String,String)]
val countries = selectCountries().map(row => 
  row[String]("code") -> row[String]("name")
).toList

En el siguiente ejemplo contaremos la cantidad de registros Country en la base de datos, de manera que el resultado será una única fila con una única columna:

// Primero obtenemos la primera fila
val firstRow = SQL("Select count(*) as c from Country").apply().head
 
// Luego obtenemos el contenido de la columna 'c' como un objeto Long
val countryCount = firstRow[Long]("c")

Utilizando Pattern Matching

También puede utilizar Pattern Matching para extraer los contenido del Row. En este caso, el nombre de la columna no importa. Tan sólo nos interesa el orden y el tipo de dato del parámetro utilizado para el Pattern Matching.

El siguiente ejemplo transforma cada fila en el tipo de dato de Scala adecuado:

case class SmallCountry(name:String) 
case class BigCountry(name:String) 
case class France
 
val countries = SQL("Select name,population from Country")().collect {
  case Row("France", _) => France()
  case Row(name:String, pop:Int) if(pop > 1000000) => BigCountry(name)
  case Row(name:String, _) => SmallCountry(name)      
}

Dado que el método collect(…) simplemente ignora los casos para los cuales no se ha definido una función parcial, permite que su código simplemente ignore las filas que no espera.

Trabajando con columnas que pueden contener valores nulos

Si de acuerdo al esquema de la base de datos una columna puede contener valores Null, deberá manipularla como un dato de tipo Option.

Por ejemplo, el campo indepYear de la tabla Country puede ser nulo, así que deberá utilizar Option[Int]` para efectuar el match:

SQL("Select name,indepYear from Country")().collect {
  case Row(name:String, Some(year:Int)) => name -> year
}

Si intenta hacer el match de esta columna como Int no podrá parsear los registros en los que el campo valga Null. Suponga que intenta obtener el contenido del campo directamente como Int del diccionario:

SQL("Select name,indepYear from Country")().map { row =>
  row[String]("name") -> row[Int]("indepYear")
}

Esto producirá una excepción de tipo UnexpectedNullableFound(COUNTRY.INDEPYEAR) si encuentra un valor nulo, así que deberá usar map para parsearla como un dato de tipo Option[Int]:

SQL("Select name,indepYear from Country")().map { row =>
  row[String]("name") -> row[Option[Int]]("indepYear")
}

Esto también es así para la API de parser, como veremos en la sección siguiente.

Usando la API parser

Puede utilizar la API parser para crear parsers que le permitan procesar los resultados de cualquier consulta de manera genérica y reutizable.

Nota: Esta es una prestación súmamente útil, dado que la mayoría de las consultas en una aplicación web retornarán conjuntos de datos similares. Por ejemplo, si ha definido un parser capaz de procesar un Country a partir de un result set, y otro parser para Language, puede fácilmente componer ambos parsers para procesar Country y Language a partir de una consulta de tipo join.

Primero tendrá que importar anorm.SqlParser._, y luego precisará un RowParser, por ejemplo a parser capaz de procesar una fila y retornar un valor Scala. Por ejemplo podemos definir un parser que transforme un resultado con una única columna a una variable Scala de tipo Long:

val rowParser = scalar[Long]

Luego debemos transformar este parser de filas (RowParser) a un parser de resultados de datos (ResultSetParser). En este caso crearemos un parsers que nos permita procesar una única fila:

val rsParser = scalar[Long].single

Así que este parser procesará un resulta ser para retornar una variable de tipo Long. Esto es útil para pcoesar el resultado de una consulta SQL de tipo select count:

val count: Long = SQL("select count(*) from Pais").as(scalar[Long].single)

Escribamos un parser más complicado:

str("nombre") ~ int("poblacion"), creará un RowParser capaz de procesar una fila que contenga una columna de tipo String llamada nombre y una columna de tipo Integer llamada poblacion. Luego podremos crear un ResultSetParser que procesará tantas filas como le sea posible utilizando *:

val populations:List[String~Int] = {
  SQL("select * from Pais").as( str("nombre") ~ int("poblacion") * ) 
}

Como puede ver, el tipo de dato del resultado de la consulta es List[String~Int] - una lista de nombres de pais y cantidad de población.

También puede escribir el mismo código de la siguiente manera:

val result:List[String~Int] = {
  SQL("select * from Pais").as(get[String]("nombre")~get[Int]("poblacion")*) 
}

¿De qué se trata el tipo de dato String-Int? Se trata de un tipo de dato de Anorm que no es de mucha utilidad fuera del código de acceso a base de datos. En este caso es preferible tener una simple tupla de tipo (String, Int). Puede utilizar la función map aplicada a un RowParser para transformar este resultado en un tipo de dato más útil:

```scala` str("nombre") ~ int("poblacion") map { case n~p => (n,p) }


> **Nota:** En este ejemplo creamos una tupla `(String,Int)`, pero nada le impide transformar el resultado del `RowParser` en otro tipo de dato, como puede ser una `case class` personalizada.

Dado que transformar datos de tipo `A~B~C` a `(A,B,C)` es una tarea muy común, hemos desarrollador una función `flatten` que hace exactamente eso. De manera que finalmente quedaría así:

```scala
val result:List[(String,Int)] = {
  SQL("select * from Pais").as(
    str("nombre") ~ int("poblacion") map(flatten) *
  ) 
}

Ahora intentemos un ejemplo más complicado. ¿Cómo haríamos para procesar el resultado de la siguiente consulta, de manera de obtener el nombre del país y todos los idiomas que se hablan en él para un código de país específico?

select p.nombre, i.idioma from Pais p 
    join PaisIdioma i on i.CountryCode = p.Code 
    where p.code = 'FRA'

Comencemos para procesar todas las filas como un dato de tipo List[(String,String)] (una lista de tuplas de nombres de países e idiomas):

var p: ResultSetParser[List[(String,String)] = {
  str("nombre") ~ str("idioma") map(flatten) *
}

Y obtendríamos un resultado como el siguiente:

List(
  ("Francia", "Arabe"), 
  ("Francia", "Frances"), 
  ("Francia", "Italiano"), 
  ("Francia", "Portugues"), 
  ("Francia", "Castellano"), 
  ("Francia", "Turco")
)

Podemos usar la API de colecciones de Scala para transformarla al tipo de dato esperado:

case class IdiomasHablados(pais:String, idiomas:Seq[String])

idiomas.headOption.map { f =>
  IdiomasHablados(f._1, idiomas.map(_._2))
}

Finalmente, nos quedaría la siguiente función:

case class IdiomasHablados(pais:String, idiomas:Seq[String])

def IdiomasHablados(paisCodigo: String): Option[IdiomasHablados] = {
  val idiomas: List[(String, String)] = SQL(
    """
      select p.nombre, i.idioma from Pais p
      join PaisIdioma i on i.PaisCodigo = p.Codigo 
      where p.codigo = {codigo};
    """
  )
  .on("codigo" -> paisCodigo)
  .as(str("nombre") ~ str("idioma") map(flatten) *)

  languages.headOption.map { f =>
    IdiomasHablados(f._1, idiomas.map(_._2))
  }
}

Para complicar aún más las cosas, separaremos el idioma oficial del resto de los idiomas hablados en ese país:

case class IdiomasHablados(
  pais:String, 
  IdiomaOficial: Option[String], 
  IdiomaOtros:Seq[String]
)

def IdiomasHablados(paisCodigo: String): Option[IdiomasHablados] = {
  val idiomas: List[(String, String, Boolean)] = SQL(
    """
      select * from Pais p 
      join PaisIdioma i on i.PaisCodigo = p.Codigo 
      where p.Codigo = {codigo};
    """
  )
  .on("codigo" -> paisCodigo)
  .as {
    str("nombre") ~ str("idioma") ~ str("esOficial") map {
      case n~l~"T" => (n,l,true)
      case n~l~"F" => (n,l,false)
    } *
  }

  idiomas.headOption.map { f =>
    IdiomasHablados(
      f._1, 
      idiomas.find(_._3).map(_._2),
      idiomas.filterNot(_._3).map(_._2)
    )
  }
}

Si intenta esto en la base de datos de ejemplo de MySQL, obtendrá:

$ IdiomasHablados("FRA")
> Some(
    IdiomasHablados(Francia,Some(Francia),List(
        Arabe, Italiano, Portugues, Castellano, Turco
    ))
)

Siguiente: Integración con otras librerías de acceso a datos

⚠️ **GitHub.com Fallback** ⚠️