本章将介绍在Lift应用程序中使用MongoDB的配方。本章中的许多代码示例可以在https://github.com/LiftCookbook/cookbook_mongo找到。
连接到MongoDB数据库
解
将Lift MongoDB依赖项添加到构建中,并使用net.liftweb.mongodb
和配置连接com.mongodb
。
在build.sbt中,将以下内容添加到libraryDependencies
:
"net.liftweb"
%%
"lift-mongodb-record"
%
liftVersion
在Boot.scala中,添加:
import
com.mongodb.
{
ServerAddress
,
Mongo
}
import
net.liftweb.mongodb.
{
MongoDB
,
DefaultMongoIdentifier
}
val
server
=
new
ServerAddress
(
"127.0.0.1"
,
27017
)
MongoDB
.
defineDb
(
DefaultMongoIdentifier
,
new
Mongo
(
server
),
"mydb"
)
这将为您提供一个名为本地MongoDB数据库的连接 mydb
。
讨论
如果您的数据库需要身份验证,使用MongoDB.defineDbAuth
:
MongoDB
.
defineDbAuth
(
DefaultMongoIdentifier
,
new
Mongo
(
server
),
"mydb"
,
"username"
,
"password"
)
一些云服务会给你一个连接的URL,如 mongodb://alex.mongohq.com:10050 / fglvBskrsdsdsDaGNs1。在这种情况下,主机和端口组成第一部分,数据库名称是/之后的部分。
如果您需要将这样的URL转换为连接,您可以通过使用java.net.URI
来解析URL并进行连接:
object
MongoUrl
{
def
defineDb
(
id
:
MongoIdentifier
,
url
:
String
)
{
val
uri
=
new
URI
(
url
)
val
db
=
uri
.
getPath
drop
1
val
server
=
new
Mongo
(
new
ServerAddress
(
uri
.
getHost
,
uri
.
getPort
))
Option
(
uri
.
getUserInfo
).
map
(
_
.
split
(
":"
))
match
{
case
Some
(
Array
(
user
,
pass
))
=>
MongoDB
.
defineDbAuth
(
id
,
server
,
db
,
user
,
pass
)
case
_
=>
MongoDB
.
defineDb
(
id
,
server
,
db
)
}
}
}
MongoUrl
.
defineDb
(
DefaultMongoIdentifier
,
"mongodb://user:pass@127.0.0.1:27017/myDb"
)
MongoDB的完整URL方案更复杂,允许多个主机和连接参数,但以前的代码可以处理可选的用户名和密码字段,并且可能足以让您启动并运行MongoDB 配置。
这DefaultMongoIdentifier
是用于标识特定连接的值。Lift将标识符映射到连接,这意味着您可以连接到多个数据库。常见的情况是单个数据库,通常分配给它DefaultMongoIdentifier
。
但是,如果您需要访问两个MongoDB数据库,则可以创建一个新的标识符并将其作为记录的一部分进行分配。例如:
object
OtherMongoIdentifier
extends
MongoIdentifier
{
def
jndiName
:
String
=
"other"
}
MongoUrl
.
defineDb
(
OtherMongoIdentifier
,
"mongodb://127.0.0.1:27017/other"
)
object
Country
extends
Country
with
MongoMetaRecord
[
Country
]
{
override
def
collectionName
=
"example.earth"
override
def
mongoIdentifier
=
OtherMongoIdentifier
}
的lift-mongodb-record
依赖关系本身依赖于另一个Lift模块,lift-mongodb
,它提供了MongoDB的连接性和其他较低级别的访问。两者都与MongoDB Java驱动程序结合在一起。
在MongoDB记录中存储哈希图
解
创建一个包含以下内容的MongoDB记录MongoMapField
:
import
net.liftweb.mongodb.record._
import
net.liftweb.mongodb.record.field._
class
Country
private
()
extends
MongoRecord
[
Country
]
with
StringPk
[
Country
]
{
override
def
meta
=
Country
object
population
extends
MongoMapField
[
Country
,Int
](
this
)
}
object
Country
extends
Country
with
MongoMetaRecord
[
Country
]
{
override
def
collectionName
=
"example.earth"
}
在这个例子中,我们正在创建一个关于一个国家的信息的记录,这population
是一个String
代表该国城市的一个关键字的地图,代表该城市的Integer
人口。
我们可以使用这样的代码片段:
class
Places
{
val
uk
=
Country
.
find
(
"uk"
)
openOr
{
val
info
=
Map
(
"Brighton"
->
134293
,
"Birmingham"
->
970892
,
"Liverpool"
->
469017
)
Country
.
createRecord
.
id
(
"uk"
).
population
(
info
).
save
}
def
facts
=
"#facts"
#>
(
for
{
(
name
,
pop
)
<-
uk
.
population
.
is
}
yield
".name *"
#>
name
&
".pop *"
#>
pop
)
}
当这个代码段被调用时,它会查找一个记录_id
,uk
或使用一些固定信息创建一个记录。使用该代码段的模板可能包括:
<div
data-lift=
"Places.facts"
>
<table>
<thead>
<tr><th>
市</th><th>
人口</th></tr>
</thead>
<tbody>
<tr
id=
"facts"
>
<td
class=
"name"
>
名称这里</td><td
class=
"pop"
>
人口</td>
</tr>
</tbody>
</table>
</div>
在MongoDB中,最终的数据结构将是:
$
mongo
cookbook
MongoDB
shell
version:
2.0
.
6
connecting
to:
cookbook
>
show
collections
example.earth
system.indexes
>
db.example.earth.find().pretty()
{
"_id"
:
"uk"
,
"population"
:
{
"Brighton"
:
134293
,
"Birmingham"
:
970892
,
"Liverpool"
:
469017
}
}
讨论
如果您没有为地图设置值,默认值为空的地图,在MongoDB中表示如下:
{
"_id"
:
"uk"
,
"population"
:
{
}
}
另一种方法是将该字段标记为可选项:
object
population
extends
MongoMapField
[
Country
,Int
](
this
)
{
override
def
optional_?
=
true
}
如果您现在无需population
设置文档,则MongoDB中的该字段将被省略:
>
db.example.earth.find();
{
"_id"
:
"uk"
}
要从您的代码段将数据附加到地图,您可以修改记录以提供新的记录Map
:
uk
.
population
(
uk
.
population
.
is
+
(
"Westminster"
->
81766
)).
update
请注意,我们在update
这里使用,而不是save
。该save
方法非常聪明,将会将一个新文档插入MongoDB集合或替换现有文档_id
。更新是不同的:它仅检测文档的更改字段并更新它们。它会将此命令发送到MongoDB以获取文档:
{
"$set"
:
{
"population"
:
{
"Brighton"
:
134293
,
"Liverpool"
:
469017
,
"Birmingham"
:
970892
,
"Westminster"
:
81766
}
}
您可能需要使用update
过save
更改现有的记录。
要访问地图的单个元素,可以使用get
(或value
):
uk
.
population
.
get
(
"San Francisco"
)
// will throw java.util.NoSuchElementException
或者您可以通过标准的Scala地图界面进行访问:
val
sf
:
Option
[
Int
]
=
uk
.
population
.
is
.
get
(
"San Francisco"
)
也可以看看
有一个邮件列表上的讨论就在有限的类型的支持MongoMapField
和它周围的一个可行的办法通过重写asDBObject
。
在MongoDB中存储枚举
解
使用EnumNameField
存储枚举的字符串值。以下是一周中的几个例子:
object
DayOfWeek
extends
Enumeration
{
type
DayOfWeek
=
Value
val
Mon
,
Tue
,
Wed
,
Thu
,
Fri
,
Sat
,
Sun
=
Value
}
我们可以用它来模拟某人的出生日期:
package
code.model
import
net.liftweb.mongodb.record._
import
net.liftweb.mongodb.record.field._
import
net.liftweb.record.field.EnumNameField
class
Birthday
private
()
extends
MongoRecord
[
Birthday
]
with
StringPk
[
Birthday
]{
override
def
meta
=
Birthday
object
dow
extends
EnumNameField
(
this
,
DayOfWeek
)
}
object
Birthday
extends
Birthday
with
MongoMetaRecord
[
Birthday
]
创建记录时,该dow
字段将期望值DayOfWeek
:
import
DayOfWeek._
Birthday
.
createRecord
.
id
(
"Albert Einstein"
).
dow
(
Fri
).
save
Birthday
.
createRecord
.
id
(
"Richard Feynman"
).
dow
(
Sat
).
save
Birthday
.
createRecord
.
id
(
"Isaac Newton"
).
dow
(
Sun
).
save
讨论
看看MongoDB中存储的内容:
>
db.birthdays.find()
{
"_id"
:
"Albert Einstein"
,
"dow"
:
"Fri"
}
{
"_id"
:
"Richard Feynman"
,
"dow"
:
"Sat"
}
{
"_id"
:
"Isaac Newton"
,
"dow"
:
"Sun"
}
该dow
值是toString
枚举id
值,而不是值:
Fri
.
toString
// java.lang.String = Fri
Fri
.
id
// Int = 4
如果要存储ID,请EnumField
改用。
请注意,其他工具,特别是Rogue,期望枚举的字符串值,而不是整数ID,因此您可能更喜欢使用EnumNameField
该原因。
也可以看看
“使用流氓”介绍Rogue。
在MongoDB记录中嵌入文档
解
使用BsonRecord
定义嵌入文档,并使用它嵌入 BsonRecordField
。以下是存储有关图像在记录中的信息的示例:
import
net.liftweb.record.field.
{
IntField
,
StringField
}
class
Image
private
()
extends
BsonRecord
[
Image
]
{
def
meta
=
Image
object
url
extends
StringField
(
this
,
1024
)
object
width
extends
IntField
(
this
)
object
height
extends
IntField
(
this
)
}
object
Image
extends
Image
with
BsonMetaRecord
[
Image
]
我们可以Image
通过BsonRecordField
以下方式引用该类的实例:
class
Country
private
()
extends
MongoRecord
[
Country
]
with
StringPk
[
Country
]
{
override
def
meta
=
Country
object
flag
extends
BsonRecordField
(
this
,
Image
)
}
object
Country
extends
Country
with
MongoMetaRecord
[
Country
]
{
override
def
collectionName
=
"example.earth"
}
关联值:
val
unionJack
=
Image
.
createRecord
.
url
(
"http://bit.ly/unionflag200"
).
width
(
200
).
height
(
100
)
Country
.
createRecord
.
id
(
"uk"
).
flag
(
unionJack
).
save
(
true
)
在MongoDB中,最终的数据结构将是:
>
db.example.earth.findOne()
{
"_id"
:
"uk"
,
"flag"
:
{
"url"
:
"http://bit.ly/unionflag200"
,
"width"
:
200
,
"height"
:
100
}
}
讨论
如果您没有在嵌入式文档上设置值,则默认值将保存为:
"flag"
:
{
"width"
:
0
,
"height"
:
0
,
"url"
:
""
}
您可以通过使图像可选来防止这种情况:
object
image
extends
BsonRecordField
(
this
,
Image
)
{
override
def
optional_?
=
true
}
以optional_?
这种方式设置,如果未设置值,则MongoDB文档的映像部分将不会被保存。然后在Scala中,您可以通过valueBox
呼叫访问该值:
val
img
:
Box
[
Image
]
=
uk
.
flag
.
valueBox
其实无论设置如何optional_?
,都可以使用这个值来访问valueBox
。
可选值的替代方法是始终为嵌入式文档提供默认值:
object
image
extends
BsonRecordField
(
this
,
Image
)
{
override
def
defaultValue
=
Image
.
createRecord
.
url
(
"http://bit.ly/unionflag200"
).
width
(
200
).
height
(
100
)
}
也可以看看
Lift维也纳BsonRecord
更详细地介绍。
MongoDB记录之间的链接
解
使用MongoRefField
诸如ObjectIdRefField
或者 StringRefField
使用该obj
调用来取消引用记录来创建引用。
例如,我们可以创建代表国家的记录,一个国家参考可以找到的地球:
class
Planet
private
()
extends
MongoRecord
[
Planet
]
with
StringPk
[
Planet
]
{
override
def
meta
=
Planet
object
review
extends
StringField
(
this
,
1024
)
}
object
Planet
extends
Planet
with
MongoMetaRecord
[
Planet
]
{
override
def
collectionName
=
"example.planet"
}
class
Country
private
()
extends
MongoRecord
[
Country
]
with
StringPk
[
Country
]
{
override
def
meta
=
Country
object
planet
extends
StringRefField
(
this
,
Planet
,
128
)
}
object
Country
extends
Country
with
MongoMetaRecord
[
Country
]
{
override
def
collectionName
=
"example.country"
}
为了使这个例子更容易遵循,我们的模型混合StringPk[Planet]
使用字符串作为我们的文档的主键,而不是更常用的MongoDB对象ID。因此,链接与a建立StringRefField
。
在一个代码段中,我们可以通过以下planet
方式来解决这个引用.obj
:
class
HelloWorld
{
val
uk
=
Country
.
find
(
"uk"
)
openOr
{
val
earth
=
Planet
.
createRecord
.
id
(
"earth"
).
review
(
"Harmless"
).
save
Country
.
createRecord
.
id
(
"uk"
).
planet
(
earth
.
id
.
is
).
save
}
def
facts
=
".country *"
#>
uk
.
id
&
".planet"
#>
uk
.
planet
.
obj
.
map
{
p
=>
".name *"
#>
p
.
id
&
".review *"
#>
p
.
review
}
}
对于该值uk
,我们查找现有记录,或者如果没有找到记录则创建一个记录。我们创建earth
一个单独的MongoDB记录,然后在该planet
领域中引用它与行星的ID。
通过该obj
方法检索引用,该方法Box[Planet]
在此示例中返回 。
讨论
当您调用obj
方法时,引用的记录将从MongoDB中获取MongoRefField
。您可以通过打开MongoDB驱动程序的日志记录来看到这一点。通过将下面你开始这样做 Boot.scala:
System
.
setProperty
(
"DEBUG.MONGO"
,
"true"
)
System
.
setProperty
(
"DB.TRACE"
,
"true"
)
完成此操作后,您首次运行上一个代码段,您的控制台将包括:
INFO: find: cookbook.example.country { "_id" : "uk"} INFO: update: cookbook.example.planet { "_id" : "earth"} { "_id" : "earth" , "review" : "Harmless"} INFO: update: cookbook.example.country { "_id" : "uk"} { "_id" : "uk" , "planet" : "earth"} INFO: find: cookbook.example.planet { "_id" : "earth"}
你在这里看到的是初始查找uk
,其次是创建earth
记录和保存uk
记录的更新。最后,在方法中调用了earth
什么时候查找。uk.obj
facts
该obj
调用将缓存planet
引用。这意味着你可以说:
".country *"
#>
uk
.
id
&
".planet *"
#>
uk
.
planet
.
obj
.
map
(
_
.
id
)
&
".review *"
#>
uk
.
planet
.
obj
.
map
(
_
.
review
)
earth
尽管obj
多次呼叫,您仍然只会看到一条记录的查询。另一方面,如果earth
在您打电话之后,在MongoDB的其他地方更新了记录,obj
您将无法看到调用中的更改,uk.obj
除非您先重新加载uk
记录。
通过参考查询
val
earth
:
Planet
=
...
val
onEarth
:
List
[
Country
]
=
Country
.
findAll
(
Country
.
planet
.
name
,
earth
.
id
.
is
)
或者在这种情况下,因为我们有String
参考,我们可以说:
val
onEarth
:
List
[
Country
]
=
Country
.
findAll
(
Country
.
planet
.
name
,
"earth"
)
更新和删除
uk
.
planet
.
obj
.
foreach
(
_
.
review
(
"Mostly harmless."
).
update
)
这将导致更改的字段被设置:
INFO: update: cookbook.example.planet { "_id" : "earth"} { "$set" : { "review" : "Mostly harmless."}}
一个uk.planet.obj
电话现在将返回一个行星与新的审查。
或者您可以用另一个替换参考:
uk
.
planet
(
Planet
.
createRecord
.
id
(
"mars"
).
save
.
id
.
is
).
save
再次注意,引用是通过记录(id.is
)的ID ,而不是记录本身。
删除引用:
uk
.
planet
(
Empty
).
save
这将删除链接,但链接指向的MongoDB记录将保留在数据库中。如果删除被引用的对象,稍后调用obj
将返回一个 Empty
框。
也可以看看
10Gen,Inc.的数据建模决策描述了与引用对象相比的嵌入文档。
使用流氓
解
您需要在构建中包含Rogue依赖关系,并将Rogue导入到代码中。
对于第一步,编辑build.sbt并添加:
"com.foursquare"
%%
"rogue"
%
"1.1.8"
intransitive
()
在你的代码中,运行import com.foursquare.rogue._
然后开始使用Rogue。例如,使用Scala控制台(请参阅“从Scala控制台运行查询”):
scala
>
import
com.foursquare.rogue.Rogue._
import
com.foursquare.rogue.Rogue._
scala
>
import
code.model._
import
code.model._
scala
>
Country
.
where
(
_
.
id
eqs
"uk"
).
fetch
res1
:
List
[
code.model.Country
]
=
List
(
class
code
.
model
.
Country
={
_id
=
uk
,
population
=
Map
(
Brighton
->
134293
,
Liverpool
->
469017
,
Birmingham
->
970892
)})
scala
>
Country
.
where
(
_
.
id
eqs
"uk"
).
count
res2
:
Long
=
1
scala
>
Country
.
where
(
_
.
id
eqs
"uk"
).
modify
(
_
.
population
at
"Brighton"
inc
1
).
updateOne
()
讨论
Rogue能够使用Lift记录中的信息来提供查询和更新记录的优雅方式。这是类型安全的,例如,如果您尝试使用查询中期望的Int
位置String
,MongoDB将允许在运行时找到结果,但Rogue可以使Scala在编译时拒绝查询:
scala
>
Country
.
where
(
_
.
id
eqs
7
).
fetch
<
console
>:
20
:
error:
type
mismatch
;
found
:
Int
(
7
)
required:
String
Country
.
where
(
_
.
id
eqs
7
).
fetch
DSL构造一个查询,然后我们fetch
将查询发送到MongoDB。最后一个方法,fetch
只是运行查询的方法之一。其他包括:
- 查询MongoDB的结果集的大小
- 显示结果中不同值的数量
- 如果有任何与查询匹配的记录,则为真
-
Option[T]
从查询 返回 -
类似
fetch
,但返回最多的limit
结果 - 修改与查询匹配的单个文档或所有文档
- 删除记录
count
countDistinct
exists
get
fetch(limit: Int)
updateOne
,updateMulti
,upsertOne
,和upsertMulti
findAndDeleteOne
和 bulkDelete_!!
查询语言本身具有表现力,探索各种查询的最佳方式就是QueryTest
Rogue的源代码。您将在GitHub项目的README中找到一个链接。
Rogue正在努力推出一系列新概念的版本2版本。如果您想尝试一下,请查看Rogue邮件列表中的说明和评论。
也可以看看
有关地缘空间查询,请参阅“存储地理空间价值”。
Rogue的README页面是一个很好的起点,并且包含一个链接,可以QueryTest
给予大量的例子查询到婴儿床。
Rogue的动机在Foursquare工程博客文章中有所描述。
存储地理空间价值
解
使用Rogue的LatLong
类在您的模型中嵌入位置信息。例如,我们可以存储这样一个城市的位置:
import
com.foursquare.rogue.Rogue._
import
com.foursquare.rogue.LatLong
class
City
private
()
extends
MongoRecord
[
City
]
with
ObjectIdPk
[
City
]
{
override
def
meta
=
City
object
name
extends
StringField
(
this
,
60
)
object
loc
extends
MongoCaseClassField
[
City
,LatLong
](
this
)
}
object
City
extends
City
with
MongoMetaRecord
[
City
]
{
import
net.liftweb.mongodb.BsonDSL._
ensureIndex
(
loc
.
name
->
"2d"
,
unique
=
true
)
override
def
collectionName
=
"example.city"
}
我们可以存储这样的值:
val
place
=
LatLong
(
50.819059
,
-
0.136642
)
val
city
=
City
.
createRecord
.
name
(
"Brighton, UK"
).
loc
(
pos
).
save
(
true
)
这将产生MongoDB中的数据,如下所示:
{
"_id"
:
ObjectId
(
"50f2f9d43004ad90bbc06b83"
),
"name"
:
"Brighton, UK"
,
"loc"
:
{
"lat"
:
50.819059
,
"long"
:
-
0.136642
}
}
讨论
MongoDB支持地理空间索引,我们通过两件事来利用这一点。首先,我们将位置信息存储在MongoDB允许的格式之一中。格式是包含坐标的嵌入式文档。我们也可以使用两个值的数组来表示点。
第二,我们正在创建类型的索引2d
,它允许我们使用MongoDB的地理空间功能,如$near
和$within
。在unique=true
中ensureIndex
,你可以控制是否突出的位置需要是唯一的(true
无重复)否(false
)。
至于唯一索引,你会注意到,我们正在呼吁save(true)
对City
在这个例子中,而不是简单的save
在大多数其他的食谱。我们可以save
在这里使用,它可以正常工作,但区别是save(true)
将写入关注级别从“正常”提高到“安全”。
有了正常的写入关注,save
一旦请求已经下线到MongoDB服务器,调用将返回。这给了一定程度的可靠性,save
如果网络已经消失, 则会失败。但是,没有指示服务器已经处理了请求。例如,如果我们尝试在与数据库中的位置完全相同的位置插入城市,则会违反索引唯一性规则,并且不会保存记录。只要save
(或save(false)
),我们的Lift应用程序将不会收到此错误,并且呼叫将静默失败。提高对“安全”的关注导致save(true)
等待MongoDB服务器的确认,这意味着应用程序将收到某些类型错误的异常。
例如,如果我们尝试插入一个重复的城市,我们的呼吁save(true)
将导致:
com
.
mongodb
.
MongoException$DuplicateKey
:
E11000
duplicate
key
error
index
:
cookbook.example.city.$loc_2d
还有其他级别写的关注,通过提供的另一个变体save
是需要一个WriteConcern
作为参数。
如果您需要删除索引,则MongoDB命令为:
db
.
example
.
city
.
dropIndex
(
"loc_2d"
)
查询
该食谱使用Rogue LatLong
类的原因是使我们能够使用Rogue DSL进行查询。假设我们已将其他城市插入我们的收藏:
>
db.example.city.find(
{}
,
{
_id:0
}
)
{
"name"
:
"London, UK"
,
"loc"
:
{
"lat"
:
51.5
,
"long"
:
-0.166667
}
}
{
"name"
:
"Brighton, UK"
,
"loc"
:
{
"lat"
:
50.819059
,
"long"
:
-0.136642
}
}
{
"name"
:
"Paris, France"
,
"loc"
:
{
"lat"
:
48.866667
,
"long"
:
2.333333
}
}
{
"name"
:
"Berlin, Germany"
,
"loc"
:
{
"lat"
:
52.533333
,
"long"
:
13.416667
}
}
{
"name"
:
"Sydney, Australia"
,
"loc"
:
{
"lat"
:
-33.867387
,
"long"
:
151.207629
}
}
{
"name"
:
"New York, USA"
,
"loc"
:
{
"lat"
:
40.714623
,
"long"
:
-74.006605
}
}
我们现在可以在伦敦500公里内找到这些城市:
import
com.foursquare.rogue.
{
LatLong
,
Degrees
}
val
centre
=
LatLong
(
51.5
,
-
0.166667
)
val
radius
=
Degrees
(
(
500
/
6378.137
).
toDegrees
)
val
nearby
=
City
.
where
(
_
.
loc
near
(
centre
.
lat
,
centre
.
long
,
radius
)
).
fetch
()
这将用这个子句来查询MongoDB:
{
"loc"
:
{
"$near"
:
[
51.5
,
-0.166667
,
4.491576420597608
]}}
伦敦,布莱顿和巴黎将在伦敦附近发现。
查询的形式是中心点和球形半径。该半径内的记录与查询匹配,最先返回最接近的记录。我们计算半径为弧度:500公里除以地球半径约6,378公里,给出了弧度角。我们将其转换Degrees
为Rogue的要求。
从Scala控制台运行查询
解
从您的项目启动控制台,调用boot()
,然后与您的模型进行交互。
例如,使用作为“连接到MongoDB数据库”的一部分开发的MongoDB记录,我们可以执行基本查询:
$ sbt ... > console [info] Compiling 1 Scala source to /cookbook_mongo/target/scala-2.9.1/classes... [info] Starting scala interpreter... [info] Welcome to Scala version 2.9.1.final ... Type in expressions to have them evaluated. Type :help for more information. scala> import bootstrap.liftweb._ import bootstrap.liftweb._ scala> new Boot().boot scala> import code.model._ import code.model._ scala> Country.findAll res2: List[code.model.Country] = List(class code.model.Country={_id=uk, population=Map(Brighton -> 134293, Liverpool -> 469017, Birmingham -> 970892)}) scala> :q
讨论
运行一切都Boot
可能有点沉重,特别是如果您启动各种服务和后台任务。我们需要做的就是定义数据库连接。例如,使用“连接到MongoDB数据库”中提供的示例代码,我们可以初始化一个连接:
scala> import bootstrap.liftweb._ import bootstrap.liftweb._ scala> import net.liftweb.mongodb._ import net.liftweb.mongodb._ scala> MongoUrl.defineDb(DefaultMongoIdentifier, "mongodb://127.0.0.1:27017/cookbook") scala> Country.findAll res2: List[code.model.Country] = List(class code.model.Country={_id=uk, population=Map(Brighton -> 134293, Liverpool -> 469017, Birmingham -> 970892)})
也可以看看
“连接到MongoDB数据库”解释了连接到MongoDB,“使用Rogue”描述了与Rogue进行查询。
MongoDB的单元测试记录
解
使用Specs2测试框架,围绕你的规范与上下文创建和连接到数据库的每个测试和试运行后,将其摧毁。
首先,创建Scala trait来设置和销毁与MongoDB的连接。我们将把这个特征与我们的规范相结合:
import
net.liftweb.http.
{
Req
,
S
,
LiftSession
}
import
net.liftweb.util.StringHelpers
import
net.liftweb.common.Empty
import
net.liftweb.mongodb._
import
com.mongodb.ServerAddress
import
com.mongodb.Mongo
import
org.specs2.mutable.Around
import
org.specs2.execute.Result
trait
MongoTestKit
{
val
server
=
new
Mongo
(
new
ServerAddress
(
"127.0.0.1"
,
27017
))
def
dbName
=
"test_"
+
this
.
getClass
.
getName
.
replace
(
"."
,
"_"
)
.
toLowerCase
def
initDb
()
:
Unit
=
MongoDB
.
defineDb
(
DefaultMongoIdentifier
,
server
,
dbName
)
def
destroyDb
()
:
Unit
=
{
MongoDB
.
use
(
DefaultMongoIdentifier
)
{
d
=>
d
.
dropDatabase
()
}
MongoDB
.
close
}
trait
TestLiftSession
{
def
session
=
new
LiftSession
(
""
,
StringHelpers
.
randomString
(
20
),
Empty
)
def
inSession
[
T
](
a
:
=>
T
)
:
T
=
S
.
init
(
Req
.
nil
,
session
)
{
a
}
}
object
MongoContext
extends
Around
with
TestLiftSession
{
def
around
[
T
<%
Result
](
testToRun
:
=>
T
)
=
{
initDb
()
try
{
inSession
{
testToRun
}
}
finally
{
destroyDb
()
}
}
}
}
此特征提供了连接到本地运行的MongoDB服务器的管道,并根据混合到其中的类的名称创建数据库。重要的部分是MongoContext
确保around
您的数据库的初始化规范,并且在您的规范运行后,它被清理。
Specs2 2.x和Scala 2.10
如果您使用Scala 2.10,您还将使用较新版本的Specs2,在这种情况下MongoContent
需要修改。对于Specs2 2.1,您将需要以下代码:
object
MongoContext
extends
Around
with
TestLiftSession
{
def
around
[
T
:
AsResult
](
testToRun
:
=>
T
)
:
Result
=
{
initDb
()
try
{
inSession
{
ResultExecution
.
execute
(
AsResult
(
testToRun
))
}
}
finally
{
destroyDb
()
}
}
}
2.x系列Specs2的变化记录在Eric Torreborre的博客上。
要在规范中使用它,请混合特征,然后添加上下文:
import
org.specs2.mutable._
class
MySpec
extends
Specification
with
MongoTestKit
{
sequential
"My Record"
should
{
"be able to create records"
in
MongoContext
{
val
r
=
MyRecord
.
createRecord
// ...your useful test here...
r
.
valueBox
.
isDefined
must
beTrue
}
}
}
> test [info] Compiling 1 Scala source to target/scala-2.9.1/test-classes... [info] My Record should [info] + be able to create records [info] [info] [info] Total for specification MySpec [info] Finished in 1 second, 199 ms [info] 1 example, 0 failure, 0 error [info] [info] Passed: : Total 1, Failed 0, Errors 0, Passed 0, Skipped 0 [success] Total time: 1 s, completed 03-Jan-2013 22:47:54
讨论
Lift通常提供您需要连接和运行的所有脚手架与MongoDB。没有运行的Lift应用程序,我们需要确保在我们的测试运行在Lift之外时配置MongoDB,这就是MongoTestKit
特质为我们提供的。
测试设置的一个不寻常的部分是包括a TestLiftSession
。这将提供一个围绕您的测试的空白会话,如果您正在访问或测试状态相关的代码(例如访问S
),这将非常有用。对于对Record进行测试并不是绝对必要的,但是由于您可能希望在某个时间点执行此操作,因此,如果您正在通过MongoDB记录测试用户登录,则将其包含在此处。
SBT有一些很好的技巧来帮助你运行测试。运行test
将运行您项目中的所有测试。如果您只想专注于一项测试,您可以:
> test-only org.example.code.MySpec
此命令还支持通配符,因此如果我们只想运行以“Mongo”开头的测试,我们可以:
> test-only org.example.code.Mongo *
还有test-quick
(在SBT 0.12中),它将仅运行上次未运行,已更改或失败~test
的测试,并观察测试中的更改并运行它们。
test-only
与修改一起around
在MongoTestKit
可以追踪你有一个测试的任何问题的好办法。通过禁用调用destroyDb()
,您可以跳转到MongoDB shell,并在运行测试后检查数据库的状态。
数据库清理
在每次测试中,我们只是删除数据库,所以下次尝试使用它时,它将为空。在某些情况下,您可能无法做到这一点。例如,如果您针对由MongoLabs或MongoHQ等公司托管的数据库运行测试,则删除数据库将意味着您无法在运行时连接到数据库。
解决方法之一是清理每个单独的集合,通过定义您需要清理和替换的集合destroyDb
,该方法将删除这些集合中的所有条目:
lazy
val
collections
:
List
[
MongoMetaRecord
[
_
]]
=
List
(
MyRecord
)
def
destroyDb
()
:
Unit
=
{
collections
.
foreach
(
_
bulkDelete_!!
new
BasicDBObject
)
MongoDB
.
close
}
请注意,收集列表是lazy
为了避免在我们初始化数据库连接之前启动Record系统。
也可以看看
Specs2站点包含示例和用户指南。
如果您喜欢使用Scala测试框架,请查看Tim Nelson的Mongo Auth Lift模块。它包括使用该框架的测试。Tim已经写的很多东西已经适应了Specs2的这个配方。
升降机MongoDB的记录库包括与Specs2测试,只是使用的变化Before
和After
,而不是around
在该配方中使用的例子。
Flapdoodle提供了一种自动下载,安装,设置和清理MongoDB数据库的方法。这种自动化是你可以在你的单元测试包,以及包括Specs2整合使用相同Before
和After
方法通过提升MongoDB的记录中使用测试。
SBT提供的测试接口(如test
命令)还支持分叉测试,设置测试用例的特定配置以及选择运行哪些测试的方法。
“Lift”维也纳更详细地介绍了单元测试和Lift会议。