hangscer

Essential Slick 学习笔记(二)

2017/06/08

Slick isn’t an ORM
Slick不是ORM框架,如果你对Hibernate等框架熟悉的话,你可以会把Slick归结为ORM一类。但是它不是,and It’s best not to think of Slick in this way.

ORM框架试图建立起模型与关系型数据库的映射。与之不同,Slick提供了更符合数据库风格(声明式)的系列工具,比如queryrowcolumn。更多细节请参阅Coming from ORM to Slick

总之,Slick的设计蛮巧妙的。

Importing Lib Code

1
2
import slick.jdbc.MySQLProfile.backend.Database
import slick.jdbc.MySQLProfile.api._

Defining our Schema

首要工作是:定义数据库模式,告诉Slick表结构是什么,以及如何将列与scala的类型相对应。

1
2
3
4
5
6
7
case class Message(id:Long=0L,sender:String,content:String)
class MessageTable(tag: Tag)extends Table[Message](tag,"t_message"){
def id=column[Long]("id",O.PrimaryKey,O.AutoInc)
def sender=column[String]("sender")
def content=column[String]("conetnt")
def * = (id,sender,content).mapTo[Message]
}

新建表(如果你还想查看生成的SQL语句还可以这样做):

1
2
3
val messages=TableQuery[MessageTable]
println(messages.schema.createStatements.mkString(" "))
db.run(messages.schema.create)

Example Query

默认是异步操作:

1
2
3
val messages=TableQuery[MessageTable]
val q=messages.filter(_.sender === "jiang2")
db.run(q.result).foreach(println)

那么该如何才能执行同步操作呢?

1
2
3
4
5
6
import scala.concurrent.Await
import scala.concurrent.duration.Duration._
val messages=TableQuery[MessageTable]
val f=db.run(messages.result)//f 为Future容器
val r=Await.result(f,Inf)
println(r)

Inserting Data

注意:以下程序中,Message是以id作为主键,且自动增长。所以在程序中写明了id值,但是仍然没有什么用。

1
2
3
4
5
6
7
8
val insertAction=messages ++= Seq(
Message(0L,"Dave1","Hello ,Hal Do you read me,HAL1"),
Message(0L,"Dave2","Hello ,Hal Do you read me,HAL2"),
Message(0L,"Dave3","Hello ,Hal Do you read me,HAL3")
)
val f=db.run(insertAction)
val r=Await.result(f,Inf)
println(r)//Some(3)

++=是函数,用于多行插入,其函数签名为:

1
def ++= (values: Iterable[U]): ProfileAction[MultiInsertResult, NoStream, Effect.Write]

Selecting Data

1
2
3
4
val queryAction=messages.result
val f=db.run(queryAction)
val r=Await.result(f,Inf)
println(r)

Use of Await.result is strongly discouraged in production code.Many web frameworkds provide direct means of working with Futures without blocking.
在产品代码中,尽可能使用异步操作。

如果你还想查看SQL语句,可以如下调用函数:

1
queryAction.statements.mkString("")

Combining Queries with For Comprehensions

1
2
3
4
5
6
7
val query=for{
m<-messages if m.id===1L && m.sender.like("jiang%")
}yield m
val queryAction=query.result
println(queryAction.statements.mkString)
val r=Await.result(db.run(queryAction),Inf)
println(r)

===是方法,源码在此:

1
2
3
4
5
6
7
8
9
10
11
12
13
trait ColumnExtensionMethods[B1, P1] extends Any with ExtensionMethods[B1, P1] {
def === [P2, R](e: Rep[P2])(implicit om: o#arg[B1, P2]#to[Boolean, R])
def =!= [P2, R](e: Rep[P2])(implicit om: o#arg[B1, P2]#to[Boolean, R])
def < [P2, R](e: Rep[P2])(implicit om: o#arg[B1, P2]#to[Boolean, R])
def <= [P2, R](e: Rep[P2])(implicit om: o#arg[B1, P2]#to[Boolean, R])
def > [P2, R](e: Rep[P2])(implicit om: o#arg[B1, P2]#to[Boolean, R])
def >= [P2, R](e: Rep[P2])(implicit om: o#arg[B1, P2]#to[Boolean, R])
def in[P2, R, C[_]](e: Query[Rep[P2], _, C])(implicit om: o#arg[B1, P2]#to[Boolean, R])
def inSet[R](seq: Traversable[B1])(implicit om: o#to[Boolean, R])
def inSetBind[R](seq: Traversable[B1])(implicit om: o#to[Boolean, R])
def between[P2, P3, R](start: Rep[P2], end: Rep[P3])(implicit om: o#arg[B1, P2]#arg[B1, P3]#to[Boolean, R])
def ifNull[B2, P2, R](e: Rep[P2])(implicit om: o#arg[B2, P2]#to[Boolean, R]): Rep[P2]
}

这样的设计真是妙啊,妙啊!!!!😏

Actions Combine

Like Query,DBIOAction is also a monad.It implements the same methods described above,and shares the same compatibility with for comprehensions.

We can combine the actions to create the data,and query results into one action.We can do this before we have a database connection,and we run the action like any other.To do this,Slick provides a number of useful action combinators.

1
2
3
4
5
6
7
8
val actions= messages.schema.drop andThen
messages.schema.create andThen
(messages ++= Seq(
Message(1L,"jiang1","hahahh1"),
Message(1L,"jiang2","hahahh3"),
Message(1L,"jiang3","hahahh4")
))
db.run(actions)

当然,还阔以用>>来代替andThen,使代码更酷炫😂:

1
2
3
4
5
6
7
val 一系列动作= messages.schema.drop >>
messages.schema.create >>
(messages ++= Seq(
Message(1L,"jiang1","hahahh1"),
Message(1L,"jiang2","hahahh3"),
Message(1L,"jiang3","hahahh4")
))

删库,然后建库,然后插入数据。

Query、Action、Future…😢Oh My!
这几个都有map、flatMap和filter等等。但是他们之间天差地别。

  • Query,用来为单个查询构件SQL语句。通过map、filter等等语句来修饰SQL语句
  • DBIOAction,用构件多个SQL语句,比如Actions Combine章节所讲。DBIOAction也可以声明事务。通过调用mapfilter等链式调用,把查询组合在一起,并且变换结果等等操作。
  • Future是一个异步容器,不多饶舌了。

大部分情况里,先创建Query,然后通过调用result或者schema等函数得到DBIOAction。在插入数据操作中,在Query对象中通过++=方法直接得到DBIOAction对象。

Selecting Data

Filtering Results:The filter Method

可以用filter方法来过滤数据集:

1
2
3
4
5
val query=messages.filter(m=>m.sender like "%jiang%")
val queryAction=query.result
db.run(queryAction).onSuccess{
case r=>println(r)
}

The Query and TableQuery Types

trait Query[M,U,C]有三个类型参数:
就刚才的filter程序实例而言:query的类型为Query[MessageTable,MessageTable#TableElementType,Seq],分别解释一下三个参数作用:

  • M 被称为混合类型,当我们调用map、filter函数时,其中的lambda表达式的参数类型就是M。
  • U 被称为未打包类型(原文为unpacked type),这个类型就是查询结果集合中的类型。
  • C 被称为集合类型,最终查询结果的类型。

TableQueryQuery的子类,定义如下:

1
class TableQuery[T <: Table[_]](cons: Tag => T) extends Query[T, T#TableElementType, Seq]

TableQuery实际上也是一个Query,其中把Table(比如MessageTable)作为它的混合类型,它的元素类型(范型参数类型)为Message

Constant Queries
当目前为止呢,我们都是从TableQuery中构建查询,这也是最常见的模式。但是,你也可以从常数中构建查询,比如select 1,这并不和任何表相关联。

Query.apply方法把一个标量提升为Query类型,我们可以如下构建查询:

1
Query(1).result.statements.mkString("")

Transforming Results

The map Method

1
2
3
val query=messages.map(_.content)
val action=query.result
db.run(action).foreach(println)
1
2
3
val query=messages.map(m=>m.content).filter(content=>content like "hah%")
val action=query.result
db.run(action).foreach(println)
1
2
val query=messages.map(m=>(m.id,m.content))
val action=query.result

还可以给元组数据结构映射到新的数据结构(通过mapTo方法):

1
2
3
4
5
val query=messages.map(m=>(m.id,m.content).mapTo[TextOnly])
val action=query.result
db.run(action).onSuccess{
case seq=>println(seq)
}

但是以下这样写法,是不行的哦!!想想为什么呢?因为Rep类型限制。

1
val query=messages.map(m=>TextOnly(m.id,m.content))

我们也可以使用列表达式:

1
val query=messages.map(m=>m.id*1000L)

exists

有时候,我们更关心数据是否存在,而不是数据的具体内容,由此,我们可以使用exists方法:

1
2
3
4
5
val query=messages.filter(m=>m.content like "jiang%").exists
val action=query.result
db.run(action).onSuccess{
case r=>println(r)
}

还可以查看具体的查询语句:

1
2
val action=query.result
println(action.statements.mkString)

Converting Queries to Actions

在执行查询之前,我们需要调用schmea.create或者schmea.drop更或者result来把Query转换为Action。Actions代表了一系列的查询。Actions签名如下:

1
DBIOAction[R,S,E]

三个范型参数分别为:

  • R 是我们所期望返回的类型(Message,Person,Boolean等等)。
  • S 指明返回结果集是流化的,还是非流化的(Streaming[T] 或者NoStream)。
  • E 是Effect类型,其中具体有ReadWriteSchemaTransactional更或者All。它的类型是被推断出来的。不需要写明。

大多数情况下,DBIO[T]只是DBIOAction[T,NoStream,Effect.All]的别名。

Executing Actions

有两种方式执行actions,分别是:

  • db.run() 执行actions,并把所有结果放入单个集合中
  • db.stream()执行actions,并把结果放入流中,由此允许我们增量处理较大的数据集,从而避免消费大量的内存。

Streaming
调用db.stream返回的是DatabasePublisher实例,而不是Future,但是DatabasePublisher.foreach方法是异步调用的。并且通过subscribe等方法可以与Akka系统集成。

想深入了解的话,可查看这篇博文:Slick documentation on streaming

Column Expressions

列表达式提供了mapfilterlikeisNull====!=等等方法。

Equality and Inequality Methods

====!=方法可以结合任意类型的Rep[T],返回Rep[Boolean]。这里有几个例子:

1
val query=messages.filter(_.sender==="jiang2")
1
val query=messages.filter(_.sender=!="jiang2")

<><=>=可以结合任意类型的参数,不仅仅是数值类型:
比较String类型:

1
2
3
val query=messages.filter(_.sender<="jiang2")
val action=query.result
Await.result(db.run(action),Inf).foreach(println)

还有比如:

1
val query=messages.filter(m=> m.sender>=m.content)

或者比较数值类型:

1
val query=messages.filter(_.id<=100L)
Scala Code Operand Types Result Type SQL Equivalent
col1 === col2 A 或 Option[A] Boolean col1 = col2
col1 =!= col2 A 或 Option[A] Boolean col1 <> col2
col1 < col2 A 或 Option[A] Boolean col1 < col2
col1 > col2 A 或 Option[A] Boolean col1 > col2
col1 <= col2 A 或 Option[A] Boolean col1 <= col2
col1 >= col2 A 或 Option[A] Boolean col1 >= col2

String Methods

Scala Code Result Type SQL Equivalent
col1.length Int char_length(col1)
col1 ++ col2 String 连接两列值
col1 like str Boolean c1 like str
col1.toUpperCase String upper(col1)
col1.toLowerCase String lower(col1)
col1.trim String trim(col1)
col1.ltrim String ltrim(col1)
col1.rtrim String rtim(col1)

Numeric Methods

这里介绍数值方法,参数同样是Rep[_]

Scala Code Operand Types Result Type SQL Equivalent
col1 + col2 A 或 Option[A] A col1 + col2
col1 - col2 A 或 Option[A] A col1 - col2
col1 * col2 A 或 Option[A] A col1 * col2
col1 / col2 A 或 Option[A] A col1 / col2
col1 % col2 A 或 Option[A] A mod(col1,col2)
col1.abs A 或 Option[A] A abs(col1)
col1.ceil A 或 Option[A] A ceil(col1)
col1.floor A 或 Option[A] A floor(col1)
col1.round A 或 Option[A] A round(col1)

Boolean Methods

Slick also provides a set of methods that operate on boolean Reps;

Boolean column methods.OPerand and result types should be interpreted as parameters to Rep[_]

Scala Code Operand Types Result Type SQL Equivalent
col1 && col2 Boolean 或 Option[Boolean] Boolean col1 and col2
col1 或 col2 Boolean 或 Option[Boolean] Boolean col1 or col2
!col1 Boolean 或 Option[Boolean] Boolean not col1

Option Methods and Type Equivalence

Slick用Option值来表示可空值(nullable)。这点以后再说。
略…

Controlling Queries:Sort,Take,and Drop

Methods for ordering,skipping,and limiting the results of a query
Scala Code SQL Equivalent
sortBy ORDER BY
take LIMIT
drop OFFSET
1
val query=messages.sortBy(m=>m.id.desc)
1
val query=messages.sortBy(m=>m.id.asc)

那么如何按多列值来排序呢?

1
val query=messages.sortBy(m=>(m.id,m.sender.asc))

take以及drop是什么,我就不多饶舌了!!!!😏