Scala slick method, której do tej pory nie rozumiem

89

Próbuję zrozumieć, jak działa Slick i czego wymaga.

Oto przykład:

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

Czy ktoś mógłby mi wyjaśnić, jaki jest cel tej *metody, co to jest <>, dlaczego unapply? a czym jest Projection - method ~'zwraca instancję Projection2?

ses
źródło

Odpowiedzi:

198

[AKTUALIZACJA] - dodano (jeszcze jedno) wyjaśnienie dotyczące forrozumień

  1. *Metoda:

    Zwraca domyślną projekcję - tak to opisujesz:

    „wszystkie kolumny (lub wartości obliczone), które mnie zwykle interesują”.

    Twoja tabela może mieć kilka pól; potrzebujesz tylko podzbioru do domyślnej projekcji. Domyślne odwzorowanie musi być zgodne z parametrami typu tabeli.

    Weźmy to pojedynczo. Bez tego <>tylko *:

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

    Tylko taka definicja tabeli pozwoli Ci tworzyć zapytania, takie jak:

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

    domyślne odwzorowanie (Int, String)prowadzi do a List[(Int, String)] dla prostych zapytań, takich jak te.

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

    Jaki to rodzaj q? To jest Queryz projekcją (String, Int). Po wywołaniu zwraca a Listz (String, Int)krotek zgodnie z projekcją.

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

    W tym przypadku zdefiniowałeś projekcję, którą chcesz, w yieldklauzuli forzrozumienia.

  2. Teraz o <>i Bar.unapply.

    Zapewnia to tak zwane odwzorowanie odwzorowania .

    Do tej pory widzieliśmy, jak zręczny pozwala na wyrażanie zapytań w Scali, które zwracają rzutowanie kolumn (lub obliczonych wartości); Dlatego wykonując te zapytania , musisz traktować wynikowy wiersz zapytania jako krotkę Scali . Typ krotki będzie zgodny z projekcją, która jest zdefiniowana (przez twoje forrozumienie, jak w poprzednim przykładzie, przez domyślną *projekcję). Dlatego field1 ~ field2zwraca projekcję Projection2[A, B]gdzie Ajest typem field1i Bjest typem 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 
    }
    

    Mamy do czynienia z krotkami, które mogą być kłopotliwe, jeśli mamy zbyt wiele kolumn. Chcielibyśmy myśleć o wynikach nie jako o TupleNobiekcie z nazwanymi polami, ale raczej o nim.

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

    Jak to działa? <>przyjmuje odwzorowanie Projection2[Int, String]i zwraca odwzorowane odwzorowanie na typ Bar. Te dwa argumenty Bar, Bar.unapply _ mówią zręcznie, jak ta (Int, String)projekcja musi być odwzorowana na klasę przypadku.

    To jest mapowanie dwukierunkowe; Barjest konstruktorem klasy przypadku, więc są to informacje potrzebne do przejścia z (id: Int, name: String)pliku do pliku Bar. A unapply jeśli zgadłeś, jest na odwrót.

    Skąd się unapplybierze? Jest to standardowa metoda Scala dostępna dla każdej zwykłej klasy przypadku - wystarczy zdefiniować Bar, Bar.unapplyktóry jest ekstraktorem, którego można użyć do odzyskania idi namektóry Barzostał zbudowany z:

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

    Więc domyślną projekcję można przypisać do klasy przypadku, której najbardziej spodziewasz się użyć:

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

    Lub możesz nawet mieć to na zapytanie:

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

    Tutaj typ q1 to a Queryz odwzorowaniem odwzorowania na Baz. Po wywołaniu zwraca a Listz Bazobiektów:

     val result: List[Baz] = q1.list
    
  3. Wreszcie, na marginesie, w .?ofercie Option Lifting - sposób Scala radzenia sobie z wartościami, które mogą nie istnieć.

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

    Co podsumowując, będzie dobrze współgrało z Twoją pierwotną definicją 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. W odpowiedzi na komentarz dotyczący sposobu, w jaki Slick używa forrozumień:

    W jakiś sposób monadom zawsze udaje się pojawić i domagać się udziału w wyjaśnieniu ...

    Do zrozumienia nie są specyficzne tylko dla zbiorów. Mogą być stosowane na wszelkiego rodzaju Monady , a zbiory są tylko jednym z wielu rodzajów typów monada dostępnych w Scala.

    Ale ponieważ kolekcje są znane, stanowią dobry punkt wyjścia do wyjaśnienia:

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

    W Scali do zrozumienia jest cukier składniowy dla wywołań metod (prawdopodobnie zagnieżdżonych): Powyższy kod jest (mniej więcej) równoważny z:

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

    W zasadzie nic z filter, map, flatMap metody (innymi słowy, Monada ) może być stosowany w forzrozumieniu w miejscu ns. Dobrym przykładem jest monada Option . Oto poprzedni przykład, w którym ta sama forinstrukcja działa zarówno na monadach, Listjak i na Optionmonadach:

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

    W ostatnim przykładzie transformacja mogłaby wyglądać tak:

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

    W Slick, zapytania są monadycznego - są tylko obiekty z map, flatMapi filtermetody. Zatem forzrozumienie (pokazane w wyjaśnieniu *metody) przekłada się po prostu na:

    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
    

    Jak widać, flatMap, mapi filtersą wykorzystywane do wygenerowania Queryprzez powtarzające się transformacji Query(Bars) przy każdym wywołaniu filteri map. W przypadku kolekcji te metody faktycznie iterują i filtrują kolekcję, ale w Slick są używane do generowania kodu SQL. Więcej szczegółów tutaj: W jaki sposób Scala Slick tłumaczy kod Scala na JDBC?

Faiz
źródło
W bloku wyjaśnień „1”: Nie jest oczywiste, że „val q =” to WrappingQuery, podczas czytania kodu wygląda jak List <Projection2>. Jak to możliwe, że przekształca się w Query ..? (Nadal bawię się twoimi wyjaśnieniami, aby zrozumieć, jak to działa. Dziękuję za to!)
ses
@ses - dodałem (trochę długie) wyjaśnienie na ten temat ... Spójrz też na stackoverflow.com/questions/13454347/monads-with-java-8/… - zdałem sobie sprawę, że to prawie ta sama treść.
Faiz,
Uwaga dla tych, którzy doświadczają tajemniczych błędów kompilacji, użyj foo.? w przypadku kolumn Option [T] lub wystąpi trudna do odczytania niezgodność typu. Dzięki, Faiz!
sventechie
1
To świetna odpowiedź ... ale byłoby wspaniale, gdyby można ją było zaktualizować dla Slick 3.0
Ixx
6

Ponieważ nikt inny nie odpowiedział, może to pomóc Ci zacząć. Nie znam zbyt dobrze Slicka.

Z dokumentacji Slicka :

Podniesione osadzanie:

Każda tabela wymaga metody * zawierającej domyślne odwzorowanie. Opisuje to, co otrzymujesz, zwracając wiersze (w postaci obiektu tabeli) z zapytania. Projekcja Slicka * nie musi pasować do tej w bazie danych. Możesz dodać nowe kolumny (np. Z obliczonymi wartościami) lub pominąć niektóre kolumny, jak chcesz. Niewniesiony typ odpowiadający projekcji * jest podawany jako parametr typu w tabeli. W przypadku prostych, niemapowanych tabel będzie to typ pojedynczej kolumny lub krotka typów kolumn.

Innymi słowy, Slick musi wiedzieć, jak postępować z wierszem zwróconym z bazy danych. Zdefiniowana przez Ciebie metoda wykorzystuje ich funkcje kombinatora analizatora składni do łączenia definicji kolumn w coś, czego można użyć w wierszu.

Dominic Bou-Samra
źródło
ook. a Rzutowanie jest po prostu reprezentacją kolumn ... na przykład: klasa końcowa Projection2 [T1, T2] (override val _1: Column [T1], override val _2: Column [T2]) rozszerza Tuple2 (_1, _2) o Projection [( T1, T2)] {..
ses
Teraz… jak to się stało: Bar ma metodę „nie stosuj”?
sesja
2
Aha .. - wszystkie klasy przypadków implementują cechę Product, a unapply jest metodą Product. Magia.
sesja