Метод scala, который я пока не могу понять

89

Я пытаюсь понять некоторые работы Slick и их требования.

Вот это пример:

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

Может ли кто-нибудь объяснить мне, какова цель *метода здесь, в чем <>и почему unapply? а что такое Projection - method ~'возвращает экземпляр Projection2?

ses
источник

Ответы:

198

[ОБНОВЛЕНИЕ] - добавлено (еще одно) объяснение forпонимания

  1. *Метод:

    Это возвращает проекцию по умолчанию - как вы описываете:

    "все столбцы (или вычисленные значения), которые меня обычно интересуют".

    В вашей таблице может быть несколько полей; вам нужно только подмножество для вашей проекции по умолчанию. Проекция по умолчанию должна соответствовать параметрам типа таблицы.

    Давайте возьмем это по одному. Без <>всего этого *:

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

    Такое определение таблицы позволит вам делать такие запросы, как:

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

    проекция по умолчанию (Int, String)ведет к List[(Int, String)] для таких простых запросов, как эти.

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

    Какой тип q? Это Queryс проекцией (String, Int). При вызове, он возвращает Listиз (String, Int)кортежей в соответствии с выступом.

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

    В этом случае вы определили проекцию, которую хотите, в yieldразделе forпонимания.

  2. Теперь по поводу <>и Bar.unapply.

    Это обеспечивает то, что называется сопоставленными проекциями .

    До сих пор мы видели, как Slick позволяет вам выражать запросы в Scala, которые возвращают проекцию столбцов (или вычисленные значения); Поэтому при выполнении этих запросов вы должны думать о строке результатов запроса как о кортеже Scala . Тип кортежа будет соответствовать заданной проекции (по вашему forмнению, как в предыдущем примере, или по *проекции по умолчанию ). Вот почему field1 ~ field2возвращает проекцию, Projection2[A, B]где A- тип field1и Bтип 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 
    }
    

    Мы имеем дело с кортежами, которые могут быть громоздкими, если у нас слишком много столбцов. Мы хотим думать о результатах не как о TupleNкаком-то объекте с именованными полями.

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

    Как это работает? <>принимает проекцию Projection2[Int, String]и возвращает отображенную проекцию для типа Bar. Два аргумента Bar, Bar.unapply _ прекрасно говорят, как эта (Int, String)проекция должна быть сопоставлена ​​с классом case.

    Это двустороннее отображение; Bar- конструктор класса case, поэтому информация, необходимая для перехода от (id: Int, name: String)к Bar. И unapply если вы догадались, все наоборот.

    Откуда unapplyвзялось? Это стандартный метод Scala, доступный для любого обычного класса case - простое определение Barдает вам, Bar.unapplyкоторый является экстрактором, который можно использовать для возврата, idи nameчто он Barбыл построен с помощью:

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

    Таким образом, ваша проекция по умолчанию может быть сопоставлена ​​с классом case, который вы больше всего ожидаете использовать:

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

    Или вы даже можете использовать его для каждого запроса:

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

    Здесь тип q1 - это Queryс нанесенной проекцией на Baz. При вызове, он возвращает Listиз Bazобъектов:

     val result: List[Baz] = q1.list
    
  3. Наконец, в стороне, .?предлагается Option Lifting - способ Scala работать со значениями, которых может не быть.

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

    Что, в заключение, будет хорошо работать с вашим исходным определением 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. В ответ на комментарий о том, как Слик использует forпонимание:

    Каким-то образом монады всегда удается появиться и потребовать участия в объяснении ...

    Ибо понимания относятся не только к коллекциям. Их можно использовать в монадах любого типа , а коллекции - это лишь один из многих типов типов монад, доступных в Scala.

    Но поскольку коллекции знакомы, они являются хорошей отправной точкой для объяснения:

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

    В Scala для понимания - это синтаксический сахар для вызовов методов (возможно, вложенных): приведенный выше код (более или менее) эквивалентен:

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

    В принципе, ничего с filter, map, flatMap методы (другими словами, Монада ) может быть использован в forпонимании вместо ns. Хорошим примером является монада Option . Вот предыдущий пример, в котором один и тот же forоператор работает как с монадами, Listтак и с Optionмонадами:

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

    В последнем примере трансформация могла бы выглядеть так:

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

    В Слике, запросы монадические - они просто объекты с map, flatMapи filterметода. Таким образом, forпонимание (показанное в объяснении *метода) просто переводится как:

    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
    

    Как вы можете видеть, flatMap, mapи filterиспользуются для генерации Queryпутем многократного преобразования Query(Bars) с каждым вызовом filterи map. В случае коллекций эти методы фактически перебирают и фильтруют коллекцию, но в Slick они используются для генерации SQL. Подробнее здесь: Как Scala Slick переводит код Scala в JDBC?

Файз
источник
В блоке объяснения «1»: не очевидно, что «val q =» - это WrappingQuery, при чтении кода он выглядит как List <Projection2>. Как это возможно преобразовать в Query ..? (Я все еще играю с вашими объяснениями, чтобы понять, как это работает. Спасибо вам за это!)
ses
@ses - добавил (немного длинное) объяснение по этому поводу ... Также посмотрите stackoverflow.com/questions/13454347/monads-with-java-8/… - Я понял, что это почти тот же контент.
Faiz
Обратите внимание на те, кто сталкивается с загадочными ошибками компиляции, используйте foo.? для столбцов Option [T], иначе несоответствие типов будет трудночитаемым. Спасибо, Фаиз!
свентечье
1
Это отличный ответ ... было бы здорово, если бы его можно было обновить для Slick 3.0
Ixx 01
6

Поскольку никто не ответил, это может помочь вам начать работу. Я не очень хорошо знаю Слика.

Из документации Slick :

Поднятое вложение:

Каждая таблица требует * метода, определяющего проекцию по умолчанию. Это описывает, что вы получаете, когда возвращаете строки (в форме объекта таблицы) из запроса. Проекция Slick * не обязательно должна совпадать с проекцией в базе данных. Вы можете добавлять новые столбцы (например, с вычисленными значениями) или опускать некоторые столбцы по своему усмотрению. Неподнятый тип, соответствующий проекции *, передается в качестве параметра типа в Table. Для простых, не отображаемых таблиц это будет один тип столбца или кортеж типов столбцов.

Другими словами, slick должен знать, как работать со строкой, возвращаемой из базы данных. Определенный вами метод использует свои функции комбинатора синтаксического анализатора для объединения определений столбцов во что-то, что можно использовать в строке.

Доминик Боу-Самра
источник
ок. а Projection - это просто представление столбцов .. например: final class Projection2 [T1, T2] (переопределить значение _1: столбец [T1], переопределить значение _2: столбец [T2]) расширяет Tuple2 (_1, _2) с помощью проекции [( T1, T2)] {..
ses
Теперь ... как так получилось: у бара есть метод «неприменить»?
ses
2
Ага .. - все классы case реализуют трейт Product, а unapply - метод Product. Магия.
ses