[更新] - 增加了有关于for
推导式的另一种解释
*
方法:
这返回默认投影——也就是描述:
“我通常感兴趣的所有列(或计算值)。”
你的表可能有几个字段;你只需要一个子集来作为默认投影。默认投影必须与表的类型参数匹配。
我们逐一来看。先不带<>
,只有*
:
object Bars extends Table[(Int, String)]("bar") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def * = id ~ name
}
仅仅像这样的一个表定义就能让你进行如下查询:
implicit val session: Session =
val result = Query(Bars).list
(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]
的投影,其中
A
是
field1
的类型,
B
是
field2
的类型。
q.list.map {
case (name, n) =>
}
Queury(Bars).list.map {
case (id, name) =>
}
我们正在处理元组,如果有太多的列可能会很麻烦。我们希望将结果视为具有命名字段的对象,而不是TupleN
。
(id ~ name)
case class Bar(id: Int, name: String)
(id ~ name <> (Bar, Bar.unapply _))
Query(Bars).list.map ( b.name )
这是如何工作的?
<>
获取投影
Projection2[Int, String]
并返回一个映射到类型
Bar
的投影。两个参数
Bar, Bar.unapply_
告诉slick如何将这个
(Int,String)
投影映射到一个样例类。
这是一个双向映射;
Bar
是案例类构造函数,因此这就是从
(id: Int,name: String)
到
Bar
所需的信息。而如果你猜对了,
unapply
则用于反转。
unapply
来自哪里?这是标准的Scala方法,适用于任何普通的case类 - 只要定义了
Bar
,就会给你一个
Bar.unapply
,它是一个
提取器,可以用来取回构建
Bar
的
id
和
name
。
val bar1 = Bar(1, "one")
val Bar(id, name) = bar1
val bars: List[Bar] =
val barNames = bars.map {
case Bar(_, name) => name
}
val x = Bar.unapply(bar1)
因此,您的默认投影可以映射到您最希望使用的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
的类型是一个带有映射projection到Baz
的Query
。
当被调用时,它会返回一个Baz
对象的List
:
val result: List[Baz] = q1.list
最后,附带一提,.?
提供了选项提升 - Scala 处理可能不存在的值的方式。
(id ~ name)
(id.? ~ name)
总之,这将很好地与您原始的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)
基本上,任何具有filter
、map
、flatMap
方法(换句话说,是一个Monad)的内容都可以在for
推导式中代替ns
使用。一个很好的例子是Option Monad。以下是前面的示例,相同的for
语句可以在List
和Option
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
在最后一个示例中,转换可能会像这样:
val result =
ns.flatMap(i => Some(i*i)).filter(i2 => i2 %2 ==0)
result =
ns.flatMap(i => evenSqr(i))
在Slick中,查询是单子的——它们只是具有
map
、
flatMap
和
filter
方法的对象。因此,在
*
方法的说明中显示的for推导式只是将其转换为:
val q =
Query(Bars).filter(b => b.id === 42).map(b => b.name ~ 1)
val r: List[(String, Int)] = q.list
正如您所看到的,flatMap
、map
和filter
被用于通过对Query(Bars)
的每次调用filter
和map
进行重复转换来生成一个Query
。在集合的情况下,这些方法实际上是迭代和过滤集合,但在Slick中,它们用于生成SQL。更多细节请参见:How does Scala Slick translate Scala code into JDBC?