迄今为止,我无法理解Scala Slick方法。

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 - 方法~返回Projection2的实例?

2个回答

197

[更新] - 增加了有关于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的类型是什么?它是一个带有投影(String, Int)Query

当调用时,它会根据投影返回一个(String, Int)元组的List

 val result: List[(String, Int)] = q.list
在这种情况下,你已经在for推导式的yield子句中定义了所需的投影。关于<>Bar.unapply,这提供了所谓的映射投影。迄今为止,我们已经看到slick如何允许你在Scala中表达查询,返回列的投影(或计算值);因此,在执行这些查询时,你必须将查询的结果行视为Scala元组。元组的类型将匹配所定义的Projection(由for推导式(如前面的例子)或默认的*投影定义)。这就是为什么field1 ~ field2返回一个Projection2[A,B]的投影,其中Afield1的类型,Bfield2的类型。
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_告诉slick如何将这个(Int,String)投影映射到一个样例类。
这是一个双向映射;Bar是案例类构造函数,因此这就是从(id: Int,name: String)Bar所需的信息。而如果你猜对了,unapply则用于反转。 unapply来自哪里?这是标准的Scala方法,适用于任何普通的case类 - 只要定义了Bar,就会给你一个Bar.unapply,它是一个提取器,可以用来取回构建Baridname
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的类型是一个带有映射projectionBazQuery。 当被调用时,它会返回一个Baz对象的List

 val result: List[Baz] = q1.list
  • 最后,附带一提,.? 提供了选项提升 - 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]
    
  • 针对关于Slick如何使用for推导式的评论:

    不知何故,单子(Monad)总是出现并要求成为解释的一部分……

    For推导式并不仅仅适用于集合。它们可以用于任何类型的单子(Monad),而集合只是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中,for推导式是方法(可能嵌套)调用的语法糖:上面的代码(或多或少)等效于:

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

    基本上,任何具有filtermapflatMap方法(换句话说,是一个Monad)的内容都可以在for推导式中代替ns使用。一个很好的例子是Option Monad。以下是前面的示例,相同的for语句可以在ListOption monad上工作:

    // (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)) 
    
    在Slick中,查询是单子的——它们只是具有mapflatMapfilter方法的对象。因此,在*方法的说明中显示的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
    

    正如您所看到的,flatMapmapfilter被用于通过对Query(Bars)的每次调用filtermap进行重复转换来生成一个Query。在集合的情况下,这些方法实际上是迭代和过滤集合,但在Slick中,它们用于生成SQL。更多细节请参见:How does Scala Slick translate Scala code into JDBC?


    在“1”解释块中:很难看出“val q =”是WrappingQuery,读代码时它看起来像List<Projection2>。它如何转换为Query..?(我仍在尝试理解它的工作原理,谢谢您!) - ses
    @ses - 添加了一个(稍微长一点的)关于这个的解释...另外,请查看https://dev59.com/F2Yr5IYBdhLWcg3wuMb9#13455602 - 我意识到它几乎是相同的内容。 - Faiz
    对于那些遇到神秘的编译错误的人,请使用foo.?来处理Option[T]列,否则你将得到一个难以阅读的类型不匹配错误。谢谢,Faiz! - sventechie
    1
    这是一个很好的答案...不过如果能更新到Slick 3.0就更好了。 - User

    6

    由于没有其他人回答,这可能有助于帮助您入门。我不太了解Slick。

    Slick文档中:

    Lifted Embedding:

    每个表都需要一个*方法,其中包含默认投影。这描述了当您从查询返回行(以表对象的形式)时会得到什么。 Slick的*投影不必与数据库中的投影匹配。您可以添加新列(例如带有计算值的列)或省略某些列。与*投影对应的非提升类型作为类型参数给出给Table。对于简单的未映射表,这将是单个列类型或列类型的元组。

    换句话说,Slick需要知道如何处理从数据库返回的行。您定义的方法使用它们的解析器组合函数将列定义组合成可用于行的内容。


    ook。投影只是列的表示方式,例如:final class Projection2[T1,T2]( override val _1: Column[T1], override val _2: Column[T2] ) extends Tuple2(_1,_2) with Projection[(T1,T2)] { .. - ses
    现在...为什么Bar有'unapply'方法? - ses
    2
    啊哈.. - 所有的 case 类都实现了 Product 特质,而 unapply 是 Product 的方法。神奇。 - ses

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接