第8章、MongoDB持续记录

本章将介绍在Lift应用程序中使用MongoDB的配方。本章中的许多代码示例可以在https://github.com/LiftCookbook/cookbook_mongo找到

连接到MongoDB数据库

问题

您要连接到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选项的连接配置(如超时设置)在Lift wiki上进行了说明

连接字符串URI格式描述了完整的MongoDB连接格式

在MongoDB记录中存储哈希图

问题

你想在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[CountryInt](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
  )
}

当这个代码段被调用时,它会查找一个记录_iduk或使用一些固定信息创建一个记录使用该代码段的模板可能包括:

<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[CountryInt](this) {
  override def optional_? = true
}

如果您现在无需population设置文档,则MongoDB中的该字段将被省略:

> db.example.earth.find();
{ "_id" : "uk" }

要从您的代码段将数据附加到地图,您可以修改记录以提供新的记录Map

uk.population(uk.population.is + ("Westminster"->81766)).update

请注意,我们在update这里使用,而不是savesave方法非常聪明,将会将一个新文档插入MongoDB集合或替换现有文档_id更新是不同的:它仅检测文档的更改字段并更新它们。它会将此命令发送到MongoDB以获取文档:

{ "$set" : { "population" : { "Brighton" : 134293 , "Liverpool" : 469017 ,
  "Birmingham" : 970892 , "Westminster" : 81766} }

您可能需要使用updatesave更改现有的记录。

要访问地图的单个元素,可以使用get(或value):

uk.population.get("San Francisco")
// will throw java.util.NoSuchElementException

或者您可以通过标准的Scala地图界面进行访问:

val sf : Option[Int] = uk.population.is.get("San Francisco")
MongoMapField可以包含什么

你应该知道,MongoMapField只支持原始类型。

该配方中使用的映射字段是打字的String => Int,但当然MongoDB会让您混合类型,例如将a String或a Boolean作为总体值。如果您修改了Lift和mix类型之外的数据库中的MongoDB记录,那么您将java.lang.ClassCastException在运行时得到一个

也可以看看

有一个邮件列表上的讨论就在有限的类型的支持MongoMapField和它周围的一个可行的办法通过重写asDBObject

在MongoDB中存储枚举

问题

您想在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记录中嵌入文档

问题

您有一个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记录之间的链接

问题

您有一个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.objfacts

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.的数据建模决策描述了与引用对象相比的嵌入文档。

使用流氓

问题

您希望使用Foursquare的类型安全域特定语言(DSL)Rogue查询和更新MongoDB记录。

您需要在构建中包含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只是运行查询的方法之一。其他包括:

count
查询MongoDB的结果集的大小
countDistinct
显示结果中不同值的数量
exists
如果有任何与查询匹配的记录,则为真
get
Option[T] 从查询 返回
fetch(limit: Int)
类似 fetch ,但返回最多的 limit 结果
updateOneupdateMultiupsertOne,和upsertMulti
修改与查询匹配的单个文档或所有文档
findAndDeleteOne 和 bulkDelete_!!
删除记录

查询语言本身具有表现力,探索各种查询的最佳方式就是QueryTestRogue的源代码。您将在GitHub项目的README中找到一个链接。

Rogue正在努力推出一系列新概念的版本2版本。如果您想尝试一下,请查看Rogue邮件列表中的说明和评论

也可以看看

有关地缘空间查询,请参阅“存储地理空间价值”

Rogue的README页面是一个很好的起点,并且包含一个链接,可以QueryTest给予大量的例子查询到婴儿床。

Rogue的动机在Foursquare工程博客文章有所描述

存储地理空间价值

问题

您要在MongoDB中存储纬度和经度信息。

使用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[CityLatLong](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$withinunique=trueensureIndex,你可以控制是否突出的位置需要是唯一的(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的要求。

也可以看看

MongoDB手册讨论地理空间指标。

从MongoDB手册中了解更多关于写入问题的信息。

从Scala控制台运行查询

问题

你想从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的单元测试记录

问题

您想要使用MongoDB编写单元测试来运行您的Lift Record代码。

使用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
    }

  }
}

您现在可以在SBT中运行测试打字test

> 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与修改一起aroundMongoTestKit可以追踪你有一个测试的任何问题的好办法。通过禁用调用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系统。

并行测试

如果您的测试正在修改数据并有可能进行交互,则您需要停止SBT并行运行测试。这样做的一个症状是显然失败的测试,或者在添加新测试时停止工作的工作测试,或似乎锁定的测试。 通过添加以下build.sbt来禁用

parallelExecution in Test := false

您会注意到,示例规范包括以下行:sequential这将禁用Specs2中同时运行所有测试的默认行为。

在IDE中运行测试

IntelliJ IDEA检测并允许您自动运行Specs2测试。使用Eclipse,您需要在规范开始时包含JUnit runner注释

import org.junit.runner.RunWith
import org.specs2.runner.JUnitRunner

@RunWith(classOf[JUnitRunner])
class MySpec extends Specification with MongoTestKit  {
...

然后,您可以在Eclipse中“运行为...”类。

也可以看看

Specs2站点包含示例和用户指南。

如果您喜欢使用Scala测试框架,请查看Tim Nelson的Mongo Auth Lift模块它包括使用该框架的测试。Tim已经写的很多东西已经适应了Specs2的这个配方。

升降机MongoDB的记录库包括与Specs2测试,只是使用的变化BeforeAfter,而不是around在该配方中使用的例子。

Flapdoodle提供了一种自动下载,安装,设置和清理MongoDB数据库的方法。这种自动化是你可以在你的单元测试包,以及包括Specs2整合使用相同BeforeAfter方法通过提升MongoDB的记录中使用测试。

SBT提供的测试接口(如test命令)还支持分叉测试,设置测试用例的特定配置以及选择运行哪些测试的方法。

“Lift”维也纳更详细地介绍了单元测试和Lift会议。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值