Scala Slick Methode kann ich bisher nicht verstehen

89

Ich versuche einige Slick-Werke zu verstehen und was es erfordert.

Hier ein Beispiel:

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

Könnte mir jemand erklären, was der Zweck der *Methode hier ist, was ist <>, warum unapply? und was ist Projection - Methode ~'gibt die Instanz von zurück Projection2?

ses
quelle

Antworten:

198

[UPDATE] - (noch eine weitere) Erklärung zum forVerständnis hinzugefügt

  1. Die *Methode:

    Dies gibt die Standardprojektion zurück - so beschreiben Sie:

    "Alle Spalten (oder berechneten Werte), an denen ich normalerweise interessiert bin ".

    Ihre Tabelle kann mehrere Felder enthalten. Sie benötigen nur eine Teilmenge für Ihre Standardprojektion. Die Standardprojektion muss mit den Typparametern der Tabelle übereinstimmen.

    Nehmen wir es einzeln. Ohne das <>Zeug nur die *:

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

    Mit einer solchen Tabellendefinition können Sie folgende Abfragen durchführen:

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

    Die Standardprojektion von (Int, String)führt zu a List[(Int, String)] für einfache Abfragen wie diese.

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

    Was ist die Art von q? Es ist ein Querymit der Projektion (String, Int). Wenn es aufgerufen wird , gibt es eine Listvon (String, Int)Tupeln gemäß der Projektion.

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

    In diesem Fall haben Sie die gewünschte Projektion in der yieldKlausel des forVerständnisses definiert.

  2. Nun zu <>und Bar.unapply.

    Dies liefert sogenannte zugeordnete Projektionen .

    Bisher haben wir gesehen, wie Sie mit Slick Abfragen in Scala ausdrücken können, die eine Projektion von Spalten (oder berechneten Werten) zurückgeben. Wenn Sie diese Abfragen ausführen , müssen Sie sich die Ergebniszeile einer Abfrage als Scala-Tupel vorstellen . Der Typ des Tupels entspricht der definierten Projektion (nach Ihrem forVerständnis wie im vorherigen Beispiel der Standardprojektion *). Aus diesem Grund wird field1 ~ field2eine Projektion zurückgegeben, bei der angegeben wird, Projection2[A, B]wo Asich der Typ befindet field1und wo sich der Typ Bbefindet 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 
    }
    

    Wir haben es mit Tupeln zu tun, was umständlich sein kann, wenn wir zu viele Spalten haben. Wir möchten Ergebnisse nicht als TupleNein Objekt mit benannten Feldern betrachten.

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

    Wie funktioniert das? <>Nimmt eine Projektion Projection2[Int, String]und gibt eine zugeordnete Projektion für den Typ zurück Bar. Die beiden Argumente zeigen Bar, Bar.unapply _ , wie diese (Int, String)Projektion einer Fallklasse zugeordnet werden muss.

    Dies ist eine bidirektionale Zuordnung. Barist der Fallklassenkonstruktor, das sind also die Informationen, die benötigt werden, um von (id: Int, name: String)zu a zu gelangen Bar. Und unapply wenn Sie es erraten haben, ist für das Gegenteil.

    Woher kommt unapplydas? Dies ist eine Standard-Scala-Methode, die für jede gewöhnliche Fallklasse verfügbar ist. WennBar Sie nur definieren, erhalten Sie einen Extraktor , mit dem Sie das zurückerhalten können und mit dem das erstellt wurde:Bar.unapplyidnameBar

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

    So kann Ihre Standardprojektion der Fallklasse zugeordnet werden, die Sie am meisten erwarten:

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

    Oder Sie können es sogar pro Abfrage haben:

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

    Hier ist der Typ q1 ein Querymit einer abgebildeter Projektion auf Baz. Beim Aufrufen wird eines Listder BazObjekte zurückgegeben:

     val result: List[Baz] = q1.list
  3. Abgesehen davon .?bietet das Angebot Option Lifting - die Scala-Methode für den Umgang mit Werten, die möglicherweise nicht vorhanden sind.

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

    Was zusammenfassend gut zu Ihrer ursprünglichen Definition von 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. Als Antwort auf den Kommentar, wie Slick forVerständnis verwendet:

    Irgendwie schaffen es Monaden immer, aufzutauchen und zu verlangen, Teil der Erklärung zu sein ...

    Zum Verständnis sind nicht nur Sammlungen spezifisch. Sie können für jede Art von Monade verwendet werden , und Sammlungen sind nur eine der vielen Arten von Monadentypen, die in Scala verfügbar sind.

    Da Sammlungen jedoch bekannt sind, bieten sie einen guten Ausgangspunkt für eine Erklärung:

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

    In Scala ist a zum Verständnis syntaktischer Zucker für Methodenaufrufe (möglicherweise verschachtelt): Der obige Code entspricht (mehr oder weniger):

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

    Im Grunde genommen alles mit filter, map, flatMap Methoden (in anderen Worten, eine Monade ) in einen verwendet wird forVerständnis anstelle von ns. Ein gutes Beispiel ist die Optionsmonade . Hier ist das vorherige Beispiel, in dem dieselbe forAnweisung sowohl für die Monaden Listals auch für die OptionMonaden gilt:

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

    Im letzten Beispiel würde die Transformation vielleicht so aussehen:

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

    In Slick, Abfragen sind monadische - sie nur Objekte mit dem sind map, flatMapund filterMethoden. Das forVerständnis (in der Erklärung der *Methode gezeigt) bedeutet also nur:

    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
    

    Wie Sie sehen können flatMap, mapund filterwerden verwendet, um Querydurch die wiederholte Transformation von Query(Bars) mit jedem Aufruf von filterund ein zu erzeugen map. Bei Sammlungen iterieren und filtern diese Methoden die Sammlung tatsächlich, in Slick werden sie jedoch zum Generieren von SQL verwendet. Weitere Details hier: Wie übersetzt Scala Slick Scala-Code in JDBC?

Faiz
quelle
Im Erklärungsblock '1': Es ist nicht offensichtlich, dass 'val q =' WrappingQuery ist, es sieht beim Lesen des Codes wie eine Liste <Projektion2> aus. Wie ist es möglich, dass es sich in Query verwandelt? (Ich spiele immer noch mit Ihren Erklärungen, um zu verstehen, wie es funktioniert. Danke dafür!)
ses
@ses - hat eine (etwas lange) Erklärung dazu hinzugefügt ... Siehe auch stackoverflow.com/questions/13454347/monads-with-java-8/… - Ich habe festgestellt, dass es fast der gleiche Inhalt ist.
Faiz
Hinweis für Benutzer mit mysteriösen Kompilierungsfehlern: Verwenden Sie foo. Bei Option [T] -Spalten wird eine schwer lesbare Typinkongruenz angezeigt. Danke, Faiz!
Sventechie
1
Dies ist eine großartige Antwort ... es wäre jedoch großartig, wenn es für Slick 3.0
Ixx
6

Da niemand anderes geantwortet hat, kann dies Ihnen den Einstieg erleichtern. Ich kenne Slick nicht sehr gut.

Aus der Slick-Dokumentation :

Aufgehobene Einbettung:

Jede Tabelle erfordert eine * -Methode, die eine Standardprojektion enthält. Dies beschreibt, was Sie zurückerhalten, wenn Sie Zeilen (in Form eines Tabellenobjekts) aus einer Abfrage zurückgeben. Die * Projektion von Slick muss nicht mit der in der Datenbank übereinstimmen. Sie können neue Spalten hinzufügen (z. B. mit berechneten Werten) oder einige Spalten weglassen, wie Sie möchten. Der nicht angehobene Typ, der der * Projektion entspricht, wird als Typparameter für die Tabelle angegeben. Bei einfachen, nicht zugeordneten Tabellen ist dies ein einzelner Spaltentyp oder ein Tupel von Spaltentypen.

Mit anderen Worten, slick muss wissen, wie man mit einer von der Datenbank zurückgegebenen Zeile umgeht. Die von Ihnen definierte Methode verwendet ihre Parser-Kombinatorfunktionen, um Ihre Spaltendefinitionen zu etwas zu kombinieren, das für eine Zeile verwendet werden kann.

Dominic Bou-Samra
quelle
ook. und Projektion ist nur eine Darstellung von Spalten. wie: letzte Klasse Projektion2 [T1, T2] (Überschreibungswert _1: Spalte [T1], Überschreibungswert _2: Spalte [T2]) erweitert Tupel2 (_1, _2) um Projektion [( T1, T2)] {..
ses
Nun ... wie kommt es, dass Bar die Methode "unapply" hat?
ses
2
Aha .. - Alle Fallklassen implementieren das Produktmerkmal und gelten nicht für die Produktmethode. Magie.
ses