método scala slick que não consigo entender até agora

89

Tento entender alguns trabalhos do Slick e o que eles exigem.

Aqui está um exemplo:

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

Alguém poderia me explicar qual é o propósito do *método aqui, o que é <>, por quê unapply? e o que é Projection - method ~'retorna a instância de Projection2?

ses
fonte

Respostas:

198

[ATUALIZAÇÃO] - adicionada (mais uma) explicação sobre as forcompreensões

  1. O *método:

    Isso retorna a projeção padrão - que é como você descreve:

    'todas as colunas (ou valores computados) que geralmente estou interessado' em.

    Sua tabela pode ter vários campos; você só precisa de um subconjunto para sua projeção padrão. A projeção padrão deve corresponder aos parâmetros de tipo da tabela.

    Vamos fazer um de cada vez. Sem as <>coisas, apenas *:

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

    Apenas uma definição de tabela como essa permitirá que você faça 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)]

    a projeção padrão de (Int, String)leva a um List[(Int, String)] para consultas simples como essas.

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

    Qual é o tipo de q? É um Querycom a projeção (String, Int). Quando chamado, ele retorna um Listde (String, Int)tuplas de acordo com a projeção.

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

    Neste caso, você definiu a projeção que deseja na yieldcláusula da forcompreensão.

  2. Agora sobre <>e Bar.unapply.

    Isso fornece o que é chamado de projeções mapeadas .

    Até agora vimos como o slick permite expressar consultas no Scala que retornam uma projeção de colunas (ou valores calculados); Portanto, ao executar essas consultas, você deve pensar na linha de resultado de uma consulta como uma tupla Scala . O tipo de tupla corresponderá à projeção definida (por sua forcompreensão como no exemplo anterior, ou pela *projeção padrão ). É por isso que field1 ~ field2retorna uma projeção de Projection2[A, B]onde Aé o tipo de field1e Bé o 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 lidando com tuplas, o que pode ser complicado se tivermos muitas colunas. Gostaríamos de pensar nos resultados não como, TupleNmas como um objeto com campos nomeados.

    (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 é que isso funciona? <>obtém uma projeção Projection2[Int, String]e retorna uma projeção mapeada no tipo Bar. Os dois argumentos Bar, Bar.unapply _ mostram como essa (Int, String)projeção deve ser mapeada para uma classe de caso.

    Este é um mapeamento bidirecional; Baré o construtor da classe de caso, então essa é a informação necessária para ir de (id: Int, name: String)para a Bar. E unapply se você adivinhou, é o contrário.

    De onde unapplyvem? Este é um método Scala padrão disponível para qualquer classe de caso comum - apenas definir Barfornece Bar.unapplyum extrator que pode ser usado para recuperar o ide com o namequal Barfoi construído:

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

    Portanto, sua projeção padrão pode ser mapeada para a classe de caso que você mais 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 _)
    }

    Ou você pode até mesmo tê-lo 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 _))

    Aqui, o tipo de q1 é a Querycom uma projeção mapeada para Baz. Quando chamado, ele retorna um Listdos Bazobjetos:

     val result: List[Baz] = q1.list
  3. Finalmente, como um aparte, o .?ofertas de elevação Opção - a maneira Scala de lidar com valores que podem não ser.

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

    O que, resumindo, funcionará muito bem com sua definição 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. Em resposta ao comentário sobre como o Slick usa as forcompreensões:

    De alguma forma, as mônadas sempre conseguem aparecer e exigem ser parte da explicação ...

    Pois as compreensões não são específicas apenas para coleções. Eles podem ser usados ​​em qualquer tipo de Mônada , e as coleções são apenas um dos muitos tipos de mônadas disponíveis no Scala.

    Mas como as coleções são familiares, elas são um bom ponto de partida para uma explicação:

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

    Em Scala, um para compreensão é um açúcar sintático para chamadas de método (possivelmente aninhadas): O código acima é (mais ou menos) equivalente a:

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

    Basicamente, qualquer coisa com filter, map, flatMap métodos (em outras palavras, uma Mônada ) pode ser usado em uma forcompreensão no lugar de ns. Um bom exemplo é a Mônada Opção . Aqui está o exemplo anterior em que a mesma forafirmação funciona tanto Listem Optionmônadas quanto em :

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

    No último exemplo, a transformação talvez fosse assim:

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

    No Slick, as consultas são monádicas - são apenas objetos com os métodos map, flatMape filter. Portanto, a forcompreensão (mostrada na explicação do *método) se traduz apenas em:

    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 você pode ver, flatMap, mape filtersão usados para gerar uma Querypela transformação repetida Query(Bars) da cada invocação de filtere map. No caso de coleções, esses métodos realmente iteram e filtram a coleção, mas no Slick eles são usados ​​para gerar SQL. Mais detalhes aqui: Como o Scala Slick traduz o código do Scala em JDBC?

Faiz
fonte
No bloco de explicação '1': Não é óbvio que 'val q =' é WrappingQuery, parece uma Lista <Projeção2> ao ler o código. Como é possível que se transforme em Consulta ..? (Ainda estou brincando com suas explicações para entender como funciona. Obrigado por isso!)
ses
@ses - adicionada uma explicação (um pouco longa) sobre isso ... Além disso, olhe para stackoverflow.com/questions/13454347/monads-with-java-8/… - Percebi que é quase o mesmo conteúdo.
Faiz
Nota para aqueles que experimentam erros de compilação misteriosos, use foo.? para colunas Opção [T] ou você obterá uma incompatibilidade de tipo difícil de ler. Obrigado, Faiz!
sventechie
1
Esta é uma ótima resposta ... seria ótimo se pudesse ser atualizado para o Slick 3.0
Ixx
6

Como ninguém mais respondeu, isso pode ajudar você a começar. Não conheço Slick muito bem.

Da documentação do Slick :

Incorporação elevada:

Cada tabela requer um método * contendo uma projeção padrão. Isso descreve o que você recebe de volta quando retorna linhas (na forma de um objeto de tabela) de uma consulta. A projeção de Slick * não precisa corresponder à do banco de dados. Você pode adicionar novas colunas (por exemplo, com valores calculados) ou omitir algumas colunas como desejar. O tipo não levantado correspondente à * projeção é fornecido como um parâmetro de tipo para Tabela. Para tabelas simples não mapeadas, será um tipo de coluna única ou uma tupla de tipos de coluna.

Em outras palavras, o slick precisa saber como lidar com uma linha retornada do banco de dados. O método que você definiu usa suas funções combinadoras de analisador para combinar suas definições de coluna em algo que pode ser usado em uma linha.

Dominic Bou-Samra
fonte
ook. e Projeção é apenas uma representação de colunas .. como: classe final Projection2 [T1, T2] (override val _1: Column [T1], override val _2: Column [T2]) estende Tuple2 (_1, _2) com Projection [( T1, T2)] {..
ses
Agora ... como é que: Bar tem o método 'cancelar'?
ses
2
Aha .. - todas as classes de caso implementam o traço Product, e cancelar a aplicação é o método Product. Magia.
ses