méthode scala slick je ne peux pas comprendre jusqu'à présent

89

J'essaie de comprendre certaines œuvres de Slick et ce qu'elles nécessitent.

Voici un exemple:

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 _)
}

Quelqu'un pourrait-il m'expliquer à *quoi sert la méthode ici, qu'est-ce que c'est <>, pourquoi unapply? et qu'est-ce que Projection - méthode ~»renvoie l'instance de Projection2?

ses
la source

Réponses:

198

[UPDATE] - ajout (encore une autre) explication sur les forcompréhensions

  1. La *méthode:

    Cela renvoie la projection par défaut - ce que vous décrivez:

    «toutes les colonnes (ou valeurs calculées) qui m'intéressent habituellement ».

    Votre table peut avoir plusieurs champs; vous n'avez besoin que d'un sous-ensemble pour votre projection par défaut. La projection par défaut doit correspondre aux paramètres de type de la table.

    Prenons-le un à la fois. Sans le <>truc, juste le *:

    // 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)
    

    Juste une définition de table comme celle-ci vous permettra de faire des requêtes comme:

    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 projection par défaut de (Int, String)conduit à un List[(Int, String)] pour des requêtes simples comme celles-ci.

    // 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.
    

    Quel est le type de q? C'est un Queryà la projection (String, Int). Lorsqu'il est invoqué, il retourne un Listde (String, Int)tuples selon la projection.

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

    Dans ce cas, vous avez défini la projection que vous souhaitez dans la yieldclause de la forcompréhension.

  2. Maintenant environ <>et Bar.unapply.

    Cela fournit ce que l'on appelle des projections mappées .

    Jusqu'à présent, nous avons vu comment slick vous permet d'exprimer des requêtes dans Scala qui renvoient une projection de colonnes (ou de valeurs calculées); Ainsi, lors de l'exécution de ces requêtes, vous devez considérer la ligne de résultat d'une requête comme un tuple Scala . Le type du tuple correspondra à la projection qui est définie (par votre forcompréhension comme dans l'exemple précédent, ou par la *projection par défaut ). C'est pourquoi field1 ~ field2renvoie une projection de Projection2[A, B]Aest le type de field1et Best le type 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 
    }

    Nous avons affaire à des tuples, qui peuvent être encombrants si nous avons trop de colonnes. Nous aimerions penser aux résultats non pas comme TupleNmais plutôt comme un objet avec des champs nommés.

    (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.

    Comment cela marche-t-il? <>prend une projection Projection2[Int, String]et renvoie une projection mappée sur le type Bar. Les deux arguments Bar, Bar.unapply _ indiquent clairement comment cette (Int, String)projection doit être mappée à une classe de cas.

    Il s'agit d'une cartographie bidirectionnelle; Barest le constructeur de la classe case, c'est donc les informations nécessaires pour passer de (id: Int, name: String)à a Bar. Et unapply si vous l'avez deviné, c'est l'inverse.

    D'où unapplyvient-il? Il s'agit d'une méthode Scala standard disponible pour toute classe de cas ordinaire - il suffit de définir Barun Bar.unapplyqui est un extracteur qui peut être utilisé pour récupérer le idet avec namelequel le a Barété construit:

    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)]

    Ainsi, votre projection par défaut peut être mappée à la classe de cas que vous vous attendez le plus à utiliser:

    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 _)
    }

    Ou vous pouvez même l'avoir par requête:

    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 _))

    Ici, le type de q1 est un Queryavec une projection mappée vers Baz. Lorsqu'elle est appelée, elle renvoie un Listdes Bazobjets:

     val result: List[Baz] = q1.list
  3. Enfin, en passant , les .?offres Option Lifting - la manière Scala de traiter des valeurs qui peuvent ne pas l'être.

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

    Ce qui, en conclusion, fonctionnera bien avec votre définition originale 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 réponse au commentaire sur la façon dont Slick utilise les forcompréhensions:

    D'une manière ou d'une autre, les monades parviennent toujours à apparaître et demandent à faire partie de l'explication ...

    Car les compréhensions ne sont pas spécifiques aux collections uniquement. Ils peuvent être utilisés sur n'importe quel type de Monad , et les collections ne sont que l'un des nombreux types de types de monades disponibles dans Scala.

    Mais comme les collections sont familières, elles constituent un bon point de départ pour une explication:

    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, ...)

    Dans Scala, un pour la compréhension est un sucre syntaxique pour les appels de méthode (éventuellement imbriqués): Le code ci-dessus est (plus ou moins) équivalent à:

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

    En gros, quoi que ce soit avec filter, map, des flatMap méthodes (autrement dit, un Monad ) peut être utilisé dans une forcompréhension à la place de ns. Un bon exemple est la monade Option . Voici l'exemple précédent où la même forinstruction fonctionne à la fois sur les monades Listet sur les Optionmonades:

    // (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

    Dans le dernier exemple, la transformation ressemblerait peut-être à ceci:

    // 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)) 

    Dans Slick, les requêtes sont monadiques - ce ne sont que des objets avec les méthodes map, flatMapet filter. Donc, la forcompréhension (montrée dans l'explication de la *méthode) se traduit simplement par:

    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

    Comme vous pouvez le voir, flatMap, mapet filtersont utilisés pour générer une Querypar la transformation répétée Query(Bars) avec chaque appel filteret map. Dans le cas des collections, ces méthodes itèrent et filtrent la collection, mais dans Slick, elles sont utilisées pour générer du SQL. Plus de détails ici: Comment Scala Slick traduit-il le code Scala en JDBC?

Faiz
la source
Dans le bloc d'explication '1': Il n'est pas évident que 'val q =' soit WrappingQuery, cela ressemble à une List <Projection2> lors de la lecture du code. Comment est-il possible qu'il se transforme en requête ..? (Je joue toujours avec vos explications pour comprendre comment cela fonctionne. Merci pour cela!)
ses
@ses - a ajouté une (un peu longue) explication à ce sujet ... Aussi, regardez stackoverflow.com/questions/13454347/monads-with-java-8 /... - J'ai réalisé que c'était presque le même contenu.
Faiz
Remarque à ceux qui rencontrent de mystérieuses erreurs de compilation, utilisez foo.? pour les colonnes Option [T] ou vous obtiendrez une incompatibilité de type difficile à lire. Merci, Faiz!
sventechie
1
C'est une excellente réponse ... ce serait bien si elle pouvait être mise à jour pour Slick 3.0
Ixx
6

Puisque personne d'autre n'a répondu, cela pourrait vous aider à démarrer. Je ne connais pas très bien Slick.

De la documentation Slick :

Incorporation levée:

Chaque table nécessite une méthode * contenant une projection par défaut. Cela décrit ce que vous obtenez lorsque vous renvoyez des lignes (sous la forme d'un objet de table) à partir d'une requête. La projection de Slick * ne doit pas nécessairement correspondre à celle de la base de données. Vous pouvez ajouter de nouvelles colonnes (par exemple avec des valeurs calculées) ou omettre certaines colonnes à votre guise. Le type non levé correspondant à la projection * est donné comme paramètre de type à Table. Pour les tables simples et non mappées, il s'agira d'un type de colonne unique ou d'un tuple de types de colonnes.

En d'autres termes, slick a besoin de savoir comment gérer une ligne renvoyée par la base de données. La méthode que vous avez définie utilise leurs fonctions de combinateur d'analyseur pour combiner vos définitions de colonne en quelque chose qui peut être utilisé sur une ligne.

Dominique Bou-Samra
la source
ook. et Projection est juste une représentation des colonnes .. comme: classe finale Projection2 [T1, T2] (override val _1: Column [T1], override val _2: Column [T2]) étend Tuple2 (_1, _2) avec Projection [( T1, T2)] {..
ses
Maintenant ... comment se fait-il que: Bar a une méthode "non-d'application"?
ses
2
Aha .. - toutes les classes de cas implémentent le trait Product, et unapply est la méthode de Product. La magie.
ses