método scala slick que no puedo entender hasta ahora

89

Intento comprender algunos trabajos de Slick y lo que requiere.

Aquí un ejemplo:

package models

case class Bar(id: Option[Int] = None, name: String)

object Bars extends Table[Bar]("bar") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)

  // This is the primary key column
  def name = column[String]("name")

  // Every table needs a * projection with the same type as the table's type parameter
  def * = id.? ~ name <>(Bar, Bar.unapply _)
}

¿Alguien podría explicarme cuál es el propósito del *método aquí, cuál es <>, por qué unapply? y ¿qué es Projection - method ~'devuelve la instancia de Projection2?

ses
fuente

Respuestas:

198

[ACTUALIZACIÓN] : se agregó (otra más) explicación sobre las forcomprensiones

  1. El *método:

    Esto devuelve la proyección predeterminada , que es como la describe:

    'todas las columnas (o valores calculados) que normalmente me interesan'.

    Su tabla puede tener varios campos; solo necesita un subconjunto para su proyección predeterminada. La proyección predeterminada debe coincidir con los parámetros de tipo de la tabla.

    Tomémoslo uno a la vez. Sin las <>cosas, solo el *:

    // First take: Only the Table Defintion, no case class:
    
    object Bars extends Table[(Int, String)]("bar") {
      def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
      def name = column[String]("name")
    
      def * = id ~ name // Note: Just a simple projection, not using .? etc
    }
    
    // Note that the case class 'Bar' is not to be found. This is 
    // an example without it (with only the table definition)
    

    Solo una definición de tabla como esa le permitirá realizar consultas como:

    implicit val session: Session = // ... a db session obtained from somewhere
    
    // A simple select-all:
    val result = Query(Bars).list   // result is a List[(Int, String)]
    

    la proyección predeterminada de (Int, String)conduce a una List[(Int, String)] para consultas simples como estas.

    // SELECT b.name, 1 FROM bars b WHERE b.id = 42;
    val q = 
       for (b <- Bars if b.id === 42) 
         yield (b.name ~ 1)
         // yield (b.name, 1) // this is also allowed: 
                              // tuples are lifted to the equivalent projection.
    

    ¿Qué tipo de q? Es una Querycon la proyección (String, Int). Cuando se invoca, devuelve un número Listde (String, Int)tuplas según la proyección.

     val result: List[(String, Int)] = q.list
    

    En este caso, ha definido la proyección que desea en la yieldcláusula de la forcomprensión.

  2. Ahora sobre <>y Bar.unapply.

    Esto proporciona lo que se llama proyecciones mapeadas .

    Hasta ahora hemos visto cómo slick le permite expresar consultas en Scala que devuelven una proyección de columnas (o valores calculados); Entonces, al ejecutar estas consultas , debe pensar en la fila de resultados de una consulta como una tupla de Scala . El tipo de la tupla coincidirá con la proyección definida (según su forcomprensión como en el ejemplo anterior, o según la *proyección predeterminada ). Esta es la razón por la que field1 ~ field2devuelve una proyección de Projection2[A, B]dónde Aes el tipo de field1y Bes el tipo de field2.

    q.list.map {
      case (name, n) =>  // do something with name:String and n:Int
    }
    
    Queury(Bars).list.map {
      case (id, name) =>  // do something with id:Int and name:String 
    }
    

    Estamos tratando con tuplas, que pueden resultar engorrosas si tenemos demasiadas columnas. Nos gustaría pensar en los resultados no como TupleNun objeto con campos con nombre, sino más bien.

    (id ~ name)  // A projection
    
    // Assuming you have a Bar case class:
    case class Bar(id: Int, name: String) // For now, using a plain Int instead
                                          // of Option[Int] - for simplicity
    
    (id ~ name <> (Bar, Bar.unapply _)) // A MAPPED projection
    
    // Which lets you do:
    Query(Bars).list.map ( b.name ) 
    // instead of
    // Query(Bars).list.map { case (_, name) => name }
    
    // Note that I use list.map instead of mapResult just for explanation's sake.
    

    ¿Como funciona esto? <>toma una proyección Projection2[Int, String]y devuelve una proyección mapeada en el tipo Bar. Los dos argumentos Bar, Bar.unapply _ dicen hábilmente cómo esta (Int, String)proyección debe mapearse a una clase de caso.

    Este es un mapeo de dos vías; Bares el constructor de la clase de caso, por lo que esa es la información necesaria para pasar de (id: Int, name: String)a Bar. Y unapply si lo has adivinado, es al revés.

    De donde unapplyviene Este es un método estándar de Scala disponible para cualquier clase de caso ordinario; simplemente definir Barle da Bar.unapplycuál es un extractor que se puede usar para recuperar el idy con el nameque Barse construyó:

    val bar1 = Bar(1, "one")
    // later
    val Bar(id, name) = bar1  // id will be an Int bound to 1,
                              // name a String bound to "one"
    // Or in pattern matching
    val bars: List[Bar] = // gotten from somewhere
    val barNames = bars.map {
      case Bar(_, name) => name
    }
    
    val x = Bar.unapply(bar1)  // x is an Option[(String, Int)]
    

    Por lo tanto, su proyección predeterminada se puede asignar a la clase de caso que más espera usar:

    object Bars extends Table[Bar]("bar") {
      def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
      def name = column[String]("name")
      def * = id ~ name <>(Bar, Bar.unapply _)
    }
    

    O incluso puede tenerlo por consulta:

    case class Baz(name: String, num: Int)
    
    // SELECT b.name, 1 FROM bars b WHERE b.id = 42;
    val q1 = 
       for (b <- Bars if b.id === 42) 
         yield (b.name ~ 1 <> (Baz, Baz.unapply _))
    

    Aquí el tipo de q1 es un Querycon una proyección mapeada a Baz. Cuando se invoca, devuelve una Listde Bazobjetos:

     val result: List[Baz] = q1.list
    
  3. Finalmente, como acotación al margen, .?ofrece Option Lifting : la forma de Scala de lidiar con valores que pueden no serlo.

     (id ~ name)   // Projection2[Int, String] // this is just for illustration
     (id.? ~ name) // Projection2[Option[Int], String]
    

    Lo cual, para terminar, funcionará muy bien con su definición original de Bar:

    case class Bar(id: Option[Int] = None, name: String)
    
    // SELECT b.id, b.name FROM bars b WHERE b.id = 42;
    val q0 = 
       for (b <- Bars if b.id === 42) 
         yield (b.id.? ~ b.name <> (Bar, Bar.unapply _))
    
    
    q0.list // returns a List[Bar]
    
  4. En respuesta al comentario sobre cómo Slick usa for comprensiones:

    De alguna manera, las mónadas siempre logran aparecer y exigen ser parte de la explicación ...

    Porque las comprensiones no son específicas de colecciones solamente. Pueden utilizarse en cualquier tipo de Mónada. , y las colecciones son solo uno de los muchos tipos de mónadas disponibles en Scala.

    Pero como las colecciones son familiares, son un buen punto de partida para una explicación:

    val ns = 1 to 100 toList; // Lists for familiarity
    val result = 
      for { i <- ns if i*i % 2 == 0 } 
        yield (i*i)
    // result is a List[Int], List(4, 16, 36, ...)
    

    En Scala, un para comprensión es azúcar sintáctico para llamadas de método (posiblemente anidadas): el código anterior es (más o menos) equivalente a:

    ns.filter(i => i*i % 2 == 0).map(i => i*i)
    

    Básicamente, cualquier cosa con filter, map, flatMap métodos (en otras palabras, una mónada ) se puede utilizar en una forcomprensión en lugar de ns. Un buen ejemplo es la mónada Option . Aquí está el ejemplo anterior donde la misma fordeclaración funciona tanto en las mónadas Listcomo en las Optionmónadas:

    // (1)
    val result = 
      for { 
        i <- ns          // ns is a List monad
        i2 <- Some(i*i)  // Some(i*i) is Option
          if i2 % 2 == 0 // filter
      } yield i2
    
    // Slightly more contrived example:
    def evenSqr(n: Int) = { // return the square of a number 
      val sqr = n*n         // only when the square is even
      if (sqr % 2 == 0) Some (sqr)
      else None
    }
    
    // (2)
    result = 
      for { 
        i <- ns  
        i2 <- evenSqr(i) // i2 may/maynot be defined for i!
      } yield i2
    

    En el último ejemplo, la transformación quizás se vería así:

    // 1st example
    val result = 
      ns.flatMap(i => Some(i*i)).filter(i2 => i2 %2 ==0)
    
    // Or for the 2nd example
    result = 
      ns.flatMap(i => evenSqr(i)) 
    

    En Slick, las consultas son monádicas: son solo objetos con los métodos map, flatMapy filter. Entonces, la forcomprensión (que se muestra en la explicación del *método) simplemente se traduce en:

    val q = 
      Query(Bars).filter(b => b.id === 42).map(b => b.name ~ 1)
    // Type of q is Query[(String, Int)]
    
    val r: List[(String, Int)] = q.list // Actually run the query
    

    Como se puede ver, flatMap, mapy filterse utilizan para generar una Querypor la transformación repetida de Query(Bars) cada invocación de filtery map. En el caso de las colecciones, estos métodos en realidad iteran y filtran la colección, pero en Slick se utilizan para generar SQL. Más detalles aquí: ¿Cómo traduce Scala Slick el código Scala a JDBC?

Faiz
fuente
En el bloque de explicación '1': No es obvio que 'val q =' sea WrappingQuery, parece una List <Projection2> mientras lee el código. ¿Cómo es posible que se transforme en Query ..? (Todavía estoy jugando con tus explicaciones para entender cómo funciona. ¡Gracias por esto!)
ses
@ses - agregó una explicación (un poco larga) sobre esto ... Además, mire stackoverflow.com/questions/13454347/monads-with-java-8/… - Me di cuenta de que es casi el mismo contenido.
Faiz
Nota para aquellos que experimenten misteriosos errores de compilación, use foo. para las columnas de Opción [T] o obtendrá una falta de coincidencia de tipo difícil de leer. ¡Gracias, Faiz!
sventechie
1
Esta es una gran respuesta ... aunque sería genial si pudiera actualizarse para Slick 3.0
Ixx
6

Como nadie más ha respondido, esto podría ayudarlo a comenzar. No conozco muy bien a Slick.

De la documentación de Slick :

Incrustación levantada:

Cada tabla requiere un método * que contiene una proyección predeterminada. Esto describe lo que obtiene cuando devuelve filas (en forma de objeto de tabla) de una consulta. La proyección de Slick * no tiene por qué coincidir con la de la base de datos. Puede agregar nuevas columnas (por ejemplo, con valores calculados) u omitir algunas columnas como desee. El tipo no elevado correspondiente a la proyección * se proporciona como parámetro de tipo a la Tabla. En el caso de tablas simples no asignadas, será un tipo de columna única o una tupla de tipos de columna.

En otras palabras, slick necesita saber cómo lidiar con una fila devuelta desde la base de datos. El método que definió usa sus funciones de combinador de analizador para combinar sus definiciones de columna en algo que se pueda usar en una fila.

Dominic Bou-Samra
fuente
ook. y Proyección es solo una representación de columnas ... como: clase final Proyección2 [T1, T2] (anular val _1: Columna [T1], anular val _2: Columna [T2]) extiende Tuple2 (_1, _2) con Proyección [( T1, T2)] {..
ses
Ahora ... ¿cómo es que: la barra tiene el método "no aplicar"?
ses
2
Ajá ... todas las clases de casos implementan el rasgo del Producto y no se aplica el método del Producto. Magia.
ses