第7章、关系数据库持久性与记录和Squeryl

Squeryl是一个对象关系映射库。它将Scala类转换为关系数据库中的表,行和列,并提供了一种编写由Scala编译器进行类型检查的类似SQL的查询的方法。Lift Squeryl Record模块将Squeryl与Record进行集成,这意味着Lift应用程序可以使用Squeryl来存储和获取数据,同时利用Record的功能,如数据验证。

本章的代码可以在https://github.com/LiftCookbook/cookbook_squeryl找到

配置Squeryl和Record

问题

您要配置Lift应用程序以使用Squeryl和Record。

在您的构建中包含Squeryl-Record依赖项,并在Boot.scala中提供数据库连接功能SquerylRecord.initWithSquerylSession

例如,要使用PostgreSQL配置Squeryl,请修改build.sbt以添加两个依赖关系,一个用于Squeryl-Record,另一个用于数据库驱动程序:

libraryDependencies ++= {
  val liftVersion = "2.5"
  Seq(
    "net.liftweb" %% "lift-webkit" % liftVersion,
    "net.liftweb" %% "lift-squeryl-record" % liftVersion,
    "postgresql" % "postgresql" % "9.1-901.jdbc4"
    ...
    )
}

Boot.scala中,我们定义一个连接并将其注册到Squeryl中:

Class.forName("org.postgresql.Driver")

def connection = DriverManager.getConnection(
  "jdbc:postgresql://localhost/mydb",
  "username", "password")

SquerylRecord.initWithSquerylSession(
  Session.create(connection, new PostgreSqlAdapter) )

所有Squeryl查询都需要在事务的上下文中运行。提供事务的一种方法是围绕所有HTTP请求配置事务。这也在Boot.scala中配置

import net.liftweb.squerylrecord.RecordTypeMode._
import net.liftweb.http.S
import net.liftweb.util.LoanWrapper

S.addAround(new LoanWrapper {
  override def apply[T](f: => T): T = {
    val result = inTransaction {
      try {
        Right(f)
      } catch {
        case e: LiftFlowOfControlException => Left(e)
      }
    }

    result match {
      case Right(r) => r
      case Left(exception) => throw exception
    }
  }
})

这安排在inTransaction范围内处理请求As Lift对重定向使用异常,我们捕获此异常并在事务完成后抛出该异常,避免在一个S.redirectTo或类似的之后回滚

讨论

您可以使用Lift提供的任何JVM持久性机制。Lift Record提供的是一个轻松的界面,围绕着Lift的CSS变换,屏幕和向导的绑定。Squeryl-Record是将Record与Squeryl连接起来的具体实现。这意味着您可以使用标准的Record对象,这些对象实际上是您的架构,使用Squeryl和编写在编译时验证的查询。

插入Squeryl意味着初始化Squeryl的会话管理,这允许我们在Squeryl transactioninTransaction函数中包装查询这两个调用之间的区别在于,inTransaction如果不存在,将会启动一个新的事务,而transaction总是创建一个新的事务。

通过确保所有HTTP请求addAround都可以进行交易,我们可以在Lift中编写查询,而在大多数情况下,不必自行建立交易,除非我们想要。例如:

import net.liftweb.squerylrecord.RecordTypeMode._
val r = myTable.insert(MyRecord.createRecord.myField(aValue))

在这个食谱中,PostgreSqlAdapter被使用。Squeryl还支持:OracleAdapterMySQLInnoDBAdapterMySQLAdapterMSSQLServerH2AdapterDB2Adapter,和DerbyAdapter

也可以看看

Squeryl 入门指南链接到有关会话管理和配置的更多信息。

请参阅“使用JNDI DataSource”通过Java命名和目录接口(JNDI)配置连接。

使用JNDI DataSource

问题

您希望为Record-Squeryl Lift 应用程序使用Java命名和目录接口(JNDI)数据源。

Boot.scala,调用initWithSquerylSessionDataSource从JNDI上下文抬头

import javax.sql.DataSource
val ds = new InitialContext().
  lookup("java:comp/env/jdbc/mydb").asInstanceOf[DataSource]

SquerylRecord.initWithSquerylSession(
  Session.create(ds.getConnection(), new MySQLAdapter) )

mydbJNDI配置中的数据库名称替换,并替换MySQLAdapter正在使用的数据库的适当适配器。

讨论

JNDI是Web容器提供的一个服务(例如,Jetty,Tomcat),允许您在容器中配置数据库连接,然后通过应用程序中的名称引用连接。这样做的一个优点是您可以避免将数据库凭据包含在您的Lift源代码库中。

JNDI的配置对于每个容器是不同的,并且可能随着您使用的容器的版本而变化。接下来的“另请参阅”部分包含指向流行容器的文档页面的链接。

某些环境也可能要求您引用src / main / webapp / WEB-INF / web.xml文件中的JNDI资源

<resource-ref>
 <res-ref-name>jdbc / mydb </res-ref-name>
 <res-type>javax.sql.DataSource </res-type>
 <res-auth>容器</res-auth>
</resource-ref>

也可以看看

JNDI配置的资源包括:

一对多关系

问题

你想建立一对多的关系,例如属于单个星球的卫星,但是可能有许多卫星的星球。

oneToManyRelation在您的模式中使用Squeryl ,并在您的Lift模型中,包括从卫星到地球的参考。

目标是模拟关系,如图7-1所示

一颗行星可能有很多卫星,但一颗卫星只能绕着一颗星球
图7-1。一颗行星可能有很多卫星,但一颗卫星只能绕着一颗星球

代码:

package code.model

import org.squeryl.Schema
import net.liftweb.record.{MetaRecord, Record}
import net.liftweb.squerylrecord.KeyedRecord
import net.liftweb.record.field.{StringField, LongField}
import net.liftweb.squerylrecord.RecordTypeMode._

object MySchema extends Schema {

  val planets = table[Planet]
  val satellites = table[Satellite]

  val planetToSatellites = oneToManyRelation(planets, satellites).
    via((p,s) => p.id === s.planetId)

  on(satellites) { s =>
    declare(s.planetId defineAs indexed("planet_idx"))
  }

  class Planet extends Record[Planet] with KeyedRecord[Long] {
    override def meta = Planet
    override val idField = new LongField(this)
    val name = new StringField(this, 256)
    lazy val satellites = MySchema.planetToSatellites.left(this)
  }

  object Planet extends Planet with MetaRecord[Planet]

  class Satellite extends Record[Satellite] with KeyedRecord[Long] {
     override def meta = Satellite
     override val idField = new LongField(this)
     val name = new StringField(this, 256)
     val planetId = new LongField(this)
     lazy val planet = MySchema.planetToSatellites.right(this)
  }

  object Satellite extends Satellite with MetaRecord[Satellite]
}

此模式基于Record类定义两个表,如table[Planet]table[Satellite]oneToManyRelation基于(viaplanetId在卫星表中建立。

这给Squeryl提供了产生外键所需的信息planetId,以限制引用行星表中现有的记录。这可以在Squeryl生成的模式中看到。我们可以在Boot.scala中打印模式

inTransaction {
  code.model.MySchema.printDdl
}

将打印:

-- table declarations :
create table Planet (
    name varchar(256) not null,
    idField bigint not null primary key auto_increment
  );
create table Satellite (
    name varchar(256) not null,
    idField bigint not null primary key auto_increment,
    planetId bigint not null
  );
-- indexes on Satellite
create index planet_idx on Satellite (planetId);
-- foreign key constraints :
alter table Satellite add constraint SatelliteFK1 foreign key (planetId)
  references Planet(idField);

planet_idx在该planetId字段上声明一个调用的索引,以提高连接期间的查询性能。

最后,我们利用的planetToSatellites.leftright方法来建立查找查询为Planet.satellitesSatellite.planet我们可以通过插入示例数据和运行查询来演示它们的用途:

inTransaction {
  code.model.MySchema.create

  import code.model.MySchema._

  val earth = planets.insert(Planet.createRecord.name("Earth"))
  val mars = planets.insert(Planet.createRecord.name("Mars"))

  // .save as a short-hand for satellite.insert when we don't need
  // to immediately reference the record (save returns Unit).
  Satellite.createRecord.name("The Moon").planetId(earth.idField.is).save
  Satellite.createRecord.name("Phobos").planetId(mars.idField.is).save

  val deimos = satellites.insert(
    Satellite.createRecord.name("Deimos").planetId(mars.idField.is) )

  println("Deimos orbits: "+deimos.planet.single.name.is)
  println("Moons of Mars are: "+mars.satellites.map(_.name.is))

}

运行此代码生成输出:

迪莫斯轨道:火星
火星的月亮是:列表(Phobos,Deimos)

在这个示例代码中,我们正在调用deimos.planet.single,它返回一个结果,如果没有找到关联的行星,将会抛出异常。headOption如果有机会找不到记录,那将是更安全的方式,因为它将评估到NoneSome[Planet]

讨论

planetToSatellites.left方法不是一个简单的Satellite对象集合。这是一个Squeryl Query[Satellite],这意味着你可以像任何其他类型一样对待它Queryable[Satellite]例如,我们可以要求在“E”之后按字母顺序排列的行星的卫星,这对于火星来说,匹配“Phobos”:

mars.satellites.where(s => s.name gt "E").map(_.name)

left方法的结果也是OneToMany[Satellite],增加了以下的方法:

assign
添加一个新的关系,但不更新数据库
associate
类似于 assign 但是更新数据库
deleteAll
删除关系

assign呼叫给出卫星到地球之间的关系:

val express = Satellite.createRecord.name("Mars Express")
mars.satellites.assign(express)
express.save

下次我们查询mars.satellites时,我们会找到火星快车轨道器。

打电话给associate我们一步,使Squeryl自动插入或更新卫星:

val express = Satellite.createRecord.name("Mars Express")
mars.satellites.associate(express)

第三种方法,deleteAll它听起来像它应该做的。它将执行以下SQL并返回删除的行数:

delete from Satellite

在一个一对多的右侧也有通过添加额外的方法ManyToOne[Planet]assigndelete请注意,要删除多对一的“一”一方,分配给记录的任何内容都将需要已经被删除,以避免由于例如离开参考不存在的行星的卫星而产生的数据库约束错误。

由于leftright是查询,这意味着每次使用它们时,你会被发送一个新的数据库查询。Squeryl将这些形式称为无国籍关系

状态的版本leftright这个样子:

class Planet extends Record[Planet] with KeyedRecord[Long] {
 ...
 lazy val satellites : StatefulOneToMany[Satellite] =
   MySchema.planetToSatellites.leftStateful(this)
}

class Satellite extends Record[Satellite] with KeyedRecord[Long] {
  ...
  lazy val planet : StatefulManyToOne[Planet] =
    MySchema.planetToSatellites.rightStateful(this)
}

这种变化意味着mars.satellites将被缓存的结果对该实例的后续调用Planet不会触发到数据库的往返。您仍然可以按照您的期望工作associate新记录或deleteAll记录,但如果在其他地方添加或更改关系,则需要调用refresh该关系才能查看更改。

你应该使用哪个版本?这将取决于您的应用程序,但如果需要,您可以在同一记录中使用。

也可以看看

Squeryl Relations页面提供了更多的细节。

多对多关系

问题

你想建立一个多对多的关系,例如许多空间探测器访问的星球,而且是一个太空探测器也访问许多行星。

manyToManyRelation在您的架构中使用Squeryl ,并实现记录来保持关系双方之间的连接。图7-2显示了我们将在此配方中创建的结构Visit,将每个许多连接到其他许多的记录在哪里

多对多:朱诺和航海家1访问了木星。 土星只被航海家1访问
图7-2。多对多:朱诺和航海家1访问了木星。土星只被航海家1访问

该模式根据两个表进行定义,一个用于行星,一个用于空间探测,另外还有两个基于第三个类之间的关系,称为Visit

package code.model

import org.squeryl.Schema
import net.liftweb.record.{MetaRecord, Record}
import net.liftweb.squerylrecord.KeyedRecord
import net.liftweb.record.field.{IntField, StringField, LongField}
import net.liftweb.squerylrecord.RecordTypeMode._
import org.squeryl.dsl.ManyToMany

object MySchema extends Schema {

  val planets = table[Planet]
  val probes = table[Probe]

  val probeVisits = manyToManyRelation(probes, planets).via[Visit] {
    (probe, planet, visit) =>
      (visit.probeId === probe.id, visit.planetId === planet.id)
  }

  class Planet extends Record[Planet] with KeyedRecord[Long] {
    override def meta = Planet
    override val idField = new LongField(this)
    val name = new StringField(this, 256)
    lazy val probes : ManyToMany[ProbeVisit] =
      MySchema.probeVisits.right(this)
  }

  object Planet extends Planet with MetaRecord[Planet]

  class Probe extends Record[Probe] with KeyedRecord[Long] {
    override def meta = Probe
    override val idField = new LongField(this)
    val name = new StringField(this, 256)
    lazy val planets : ManyToMany[PlanetVisit] =
      MySchema.probeVisits.left(this)
  }

  object Probe extends Probe with MetaRecord[Probe]

  class Visit extends Record[Visit] with KeyedRecord[Long] {
    override def meta = Visit
    override val idField = new LongField(this)
    val planetId = new LongField(this)
    val probeId = new LongField(this)
  }

  object Visit extends Visit with MetaRecord[Visit]
}

Boot.scala中,我们可以打印出这个模式:

inTransaction {
  code.model.MySchema.printDdl
}

这将产生这样的东西,具体取决于使用的数据库:

-- table declarations :
create table Planet (
    name varchar(256) not null,
    idField bigint not null primary key auto_increment
  );
create table Probe (
    name varchar(256) not null,
    idField bigint not null primary key auto_increment
  );
create table Visit (
    idField bigint not null primary key auto_increment,
    planetId bigint not null,
    probeId bigint not null
  );
-- foreign key constraints :
alter table Visit add constraint VisitFK1 foreign key (probeId)
  references Probe(idField);
alter table Visit add constraint VisitFK2 foreign key (planetId)
  references Planet(idField);

请注意,visit表将为a planetId之间的每个关系持有一行probeId

Planet.probesProbe.planets提供associate建立新关系方法。例如,我们可以建立一套行星和探测器:

val jupiter = planets.insert(Planet.createRecord.name("Jupiter"))
val saturn = planets.insert(Planet.createRecord.name("Saturn"))
val juno = probes.insert(Probe.createRecord.name("Juno"))
val voyager1 = probes.insert(Probe.createRecord.name("Voyager 1"))

然后连接它们:

juno.planets.associate(jupiter)
voyager1.planets.associate(jupiter)
voyager1.planets.associate(saturn)

我们也可以使用Probe.planets,并Planet.probes作为查询,以查找关联。要访问片段中访问过每个行星的所有探测器,我们可以这样写:

package code.snippet

class ManyToManySnippet {
  def render =
    "#planet-visits" #> planets.map { planet =>
      ".planet-name *" #> planet.name.is &
      ".probe-name *" #> planet.probes.map(_.name.is)
    }
}

该片段可以与以下模板组合使用:

<div data-lift="ManyToManySnippet">
  <h1>地球事实</h1>
  <div id="planet-visits">
    <p>
      <span class="planet-name">名称将在这里</span>被访问:
     </p>
    <ul>
      <li class="probe-name">探测器名称在这里</li>
    </ul>
  </div>
</div>

图7-3的上半部分给出了该代码段和模板的输出示例。

讨论

该Squeryl DSL manyToManyRelation(probes, planets).via[Visit]是这里的核心元件连接我们PlanetProbeVisit记录在一起。它允许我们访问了“左”和“右”的关系,双方在我们的模型Probe.planetsPlanet.probes

一对多关系的“一对多关系”一样,左侧和右侧都是查询。当您要求时Planet.probes,数据库将通过Visit记录中的加入进行适当的查询:

Select
  Probe.name,
  Probe.idField
From
  Visit,
  Probe
Where
  (Visit.probeId = Probe.idField) and (Visit.planetId = ?)

同样如“一对多关系”中所述,有查询结果的状态变体leftright缓存。

在我们插入数据库的数据中,我们没有提及VisitSqueryl manyToManyRelation有足够的信息知道如何插入访问作为关系。顺便说一句,我们以多对多的关系打电话是无关紧要的。以下两个表达式是等效的,并导致相同的数据库结构:

juno.planets.associate(jupiter)
// ..or..
jupiter.probes.associate(juno)

你甚至可能想知道为什么我们不得不打扰一下Visit记录,但这样做有好处。例如,您可以在连接表上附加附加信息,例如探测器访问星球的年份。

为此,我们修改记录以包括附加字段:

class Visit extends Record[Visit] with KeyedRecord[Long] {
  override def meta = Visit
  override val idField = new LongField(this)
  val planetId = new LongField(this)
  val probeId = new LongField(this)
  val year = new IntField(this)
}

Visit仍然是一个容器planetIdprobeId参考,但我们也有一个平均的整数持有人的访问年份。

记录访问年份,我们需要assign提供方法ManyToMany[T]这将建立关系,但不会更改数据库。相反,它返回实例Visit,我们可以更改然后存储在数据库中:

probeVisits.insert(voyager1.planets.assign(saturn).year(1980))

assign在这种情况下Visit返回类型,并Visit具有一个year字段。通过插入Visit记录probeVisits将在表中创建一行用于访问。

要访问Visit对象上的这些额外信息,可以使用以下几种方法ManyToMany[T]

associations
查询返回的 Visit 相关对象 Planet.probes Probe.planets
associationMap
一个查询返回的对 (Planet,Visit) (Probe,Visit) 根据您在其上调用的连接的哪一方( probes planets

例如,在代码片段中,我们可以列出所有的空间探测器,并且对于每个探测器,可以显示它访问的星球以及它在那一年的行星。该片段将如下所示:

"#probe-visits" #> probes.map { probe =>
  ".probe-name *" #> probe.name.is &
  ".visit" #> probe.planets.associationMap.collect {
    case (planet, visit) =>
      ".planet-name *" #> planet.name.is &
      ".year" #> visit.year.is
    }
}

我们在collect这里使用,而不是map仅仅匹配(Planet,Visit)元组,并赋予值有意义的名称。(for { (planet, visit) <- probe.planets.associationMap } yield ...)如果你愿意也可以使用

图7-3的下半部分演示了与以下模板组合时该片段的呈现方式:

<h1>探测事实</h1>

<div id="probe-visits">
  <p><span class="probe-name">太空船名称</span>访问:</p>
  <ul>
    <li class="visit">
      <span class="planet-name">这里</span>名字<span class="year">n</span>
    </li>
  </ul>
</div>
使用此配方中的多对多功能的输出示例
图7-3。使用此配方中的多对多功能的输出示例

要删除关联,使用dissociatedissociateAll方法上leftright查询。删除单个关联

val numRowsChanged = juno.planets.dissociate(jupiter)

这将在SQL中执行:

delete from Visit
where
  probeId = ? and planetId = ?

删除所有关联:

val numRowsChanged = jupiter.probes.dissociateAll

SQL的这个是:

delete from Visit
where
  Visit.planetId = ?

你不能做的是删除一个PlanetProbe如果该记录在关系中仍然有Visit关联。你会得到的是引用完整性异常。相反,您需要dissociateAll首先

jupiter.probes.dissociateAll
planets.delete(jupiter.id)

但是,如果您需要级联删除,则可以通过覆盖模式中的默认行为来实现此目的:

// To automatically remove probes when we remove planets:
probeVisits.rightForeignKeyDeclaration.constrainReference(onDelete cascade)

// To automatically remove planets when we remove probes:
probeVisits.leftForeignKeyDeclaration.constrainReference(onDelete cascade)

这是模式的一部分,因为它会改变表约束,同时printDdl生成这个(取决于你使用的数据库)

alter table Visit add constraint VisitFK1 foreign key (probeId)
  references Probe(idField) on delete cascade;

alter table Visit add constraint VisitFK2 foreign key (planetId)
  references Planet(idField) on delete cascade;

也可以看看

“一对多关系”,一对多的关系,讨论leftStatefulrightStateful关系,也适用于多对多关系。

外部密钥和级联删除在Squeryl Relations页面上有描述

向字段添加验证

问题

您要为模型中的字段添加验证,以便向用户通知您的应用程序不可接受的缺少的字段或字段。

覆盖validations您的字段上方法,并提供一个或多个验证功能。

例如,假设我们有一个行星数据库,我们希望确保用户输入的任何新行星的名称至少有五个字符。我们将此作为我们记录的验证:

 class Planet extends Record[Planet] with KeyedRecord[Long]   {
    override def meta = Planet
    override val idField = new LongField(this)

    val name = new StringField(this, 256) {
      override def validations =
        valMinLen(5, "Name too short") _ :: super.validations
    }
  }

要检查验证,在我们的代码片段中,我们调用validate记录,这将返回记录的所有错误:

package code
package snippet

import net.liftweb.http.{S,SHtml}
import net.liftweb.util.Helpers._

import model.MySchema._

class ValidateSnippet {

  def render = {

    val newPlanet = Planet.createRecord

    def validateAndSave() : Unit = newPlanet.validate match {
      case Nil =>
        planets.insert(newPlanet)
        S.notice("Planet '%s' saved" format newPlanet.name.is)

      case errors =>
        S.error(errors)
    }

    "#planetName" #> newPlanet.name.toForm &
    "type=submit" #> SHtml.onSubmitUnit(validateAndSave)
  }
}

当代码段运行时,我们渲染该Planet.name字段并连接一个提交按钮来调用该validateAndSave方法。

如果newPlanet.validate呼叫表示没有错误(Nil),我们可以保存记录并通过通知通知用户。如果有错误,我们会将它们全部呈现S.error

相应的模板可以是:

<html>
<head>
  <title>行星名称验证</title>
</head>
<body data-lift-content-id="main">
<div id="main" data-lift="surround?with=default;at=content">
  <h1>添加行星</h1>

  <div data-lift="Msgs?showAll=false">
    <lift:notice_class>noticeBox </ lift:notice_class>
  </div>

  <p>
    行星名称至少需要5个字符。
  </p>

  <form class="ValidateSnippet?form">

    <div>
      <label for="planetName">行星名称:</label>
      <input id="planetName" type="text"></input>
      <span data-lift="Msg?id=name_id&errorClass=error">
        消息出现在这里
      </span>
    </div>

    <input type="submit"></input>

  </form>

</div>
</body>
</html>

在此模板中,错误消息显示在该input字段旁边,以CSS类设计errorClass成功通知显示在页面顶部附近,正好在<h1>标题的下方,使用了一种称为的样式noticeBox

讨论

内置的验证是

valMinLen
验证字符串至少是给定的长度,如前所示
valMaxLen
验证字符串不高于给定长度
valRegex
验证字符串与给定模式匹配

一个字段上的正则表达式验证的例子是

import java.util.regex.Pattern

val url = new StringField(this, 1024) {
  override def validations =
    valRegex( Pattern.compile("^https?://.*"),
              "URLs should start http:// or https://") _ ::
    super.validations
}

来自validate类型的错误列表List[FieldError]S.error方法接受此列表并注册每个验证错误消息,以便可以在页面上显示。它通过将该消息与该字段的ID相关联来实现,从而允许您仅仅选择单个字段的错误,就像我们在该配方中所做的那样。该ID存储在该字段中,在这种情况下Planet.name,它可用作为Planet.name.uniqueFieldId这是Box[String]一个有价值的Full("name_id")name_id我们在lift:Msg?id=name_id&errorClass=error标记中使用的这个值就是选择这个字段的错误。

您不必使用S.error显示验证信息。您可以滚动自己的显示代码,FieldError直接使用从源代码中可以看到FieldError,该错误可作为msg属性使用:

case class FieldError(field: FieldIdentifier, msg: NodeSeq) {
  override def toString = field.uniqueFieldId + " : " + msg
}

也可以看看

BaseField.scala在Lift的源代码中包含了内置的定义StringValidators

第3章介绍表单处理,通知和错误。

自定义验证逻辑

问题

您要提供自己的验证逻辑并将其应用于记录中的一个字段。

从字段的类型实现一个函数List[FieldError],并引用该字段中 的函数validations

这里有一个例子:我们有一个行星数据库,当用户进入一个新的行星时,我们希望这个名字是唯一的。行星的名字是a String,所以我们需要提供一个功能String => List[FieldError]

随着定义的验证函数(valUnique下一个),我们将其包含在列表中validations的 name字段:

import net.liftweb.util.FieldError

class Planet extends Record[Planet] with KeyedRecord[Long] {
  override def meta = Planet
  override val idField = new LongField(this)

  val name = new StringField(this, 256) {
    override def validations =
      valUnique("Planet already exists") _ ::
      super.validations
  }

  private def valUnique(errorMsg: => String)(name: String): List[FieldError] =
    Planet.unique_?(name) match {
      case true => FieldError(this.name, errorMsg) :: Nil
      case false => Nil
    }
}

object Planet extends Planet with MetaRecord[Planet] {
  def unique_?(name: String) = from(planets) { p =>
    where(lower(p.name) === lower(name)) select(p)
  }.isEmpty
}

正如任何其他验证一样触发验证,如“向验证字段添加验证”中所述

讨论

按照惯例,验证函数有两个参数列表:第一个为错误消息,第二个为接收要验证的值。这允许您轻松地在其他字段上重用您的验证功能。例如,如果要验证卫星具有唯一的名称,您可以使用完全相同的功能,但提供不同的错误消息。

FieldError你的回报需要知道它适用于现场以及显示信息。在该示例中,该字段是name,但是我们已经习惯this.name了避免与name传递给valUnique函数参数混淆

示例代码使用错误消息的文本,但是有一个变体FieldError接受NodeSeq如果需要,这可以让您生成安全标记作为错误的一部分。例如:

FieldError(this.name, <p>Please see <a href="/policy">our name policy</a></p>)

对于国际化,您可能更喜欢将密钥传递给验证功能,并解决它通过S.?

val name = new StringField(this, 256) {
    override def validations =
      valUnique("validation.planet") _ ::
      super.validations
  }

// ...combined with...

private def valUnique(errorKey: => String)(name: String): List[FieldError] =
  Planet.unique_?(name) match {
    case false => FieldError(this.name, S ? errorKey) :: Nil
    case true => Nil
  }

也可以看看

“向现场添加验证”讨论了现场验证和内置验证。

文本本地化在Lift wiki上讨论

在设置之前修改一个字段值

问题

您要在存储之前修改一个字段的值(例如,通过删除前导和尾部空白来清除该值)。

覆盖setFilter并提供应用于该字段的功能列表。

要删除用户输入的前导和尾随空格,该字段将使用trim过滤器:

val name = new StringField(this, 256) {
   override def setFilter = trim _ :: super.setFilter
}

讨论

内置的过滤器有:

crop
通过截断强制字段的最小和最大长度
trim
适用 String.trim 于字段值
toUpper 和 toLower
更改字段值的大小写
removeRegExChars
删除匹配的正则表达式字符
notNull
将空值转换为空字符串

过滤器在验证之前运行。这意味着如果您有最小长度验证和修剪过滤器,例如,用户不能通过在其输入值的末尾包含空格来传递验证测试。

一个String字段的过滤器将是类型String => String,并且setFilter函数期望其中List的一个。知道这一点,直接写定制过滤器。例如,这是一个过滤器,在我们的name字段上应用一个简单的标题案例形式

 def titleCase(in: String) =
  in.split("\\s").
  map(_.toList).
  collect {
    case x :: xs  => (Character.toUpperCase(x).toString :: xs).mkString
  }.mkString(" ")

该功能是将空格中的输入字符串分割,将每个单词转换成字符列表,将第一个字符转换为大写,然后将字符串粘贴在一起。

我们安装titleCase在像任何其他过滤器这样的领域:

val name = new StringField(this, 256) {
   override def setFilter =
    trim _ :: titleCase _ :: super.setFilter
}

现在当用户输入“jaglan beta”作为行星名称时,它将作为“Jaglan Beta”存储在数据库中。

也可以看看

了解过滤器的最佳位置是在特质StringValidators来源BaseField

如果您确实需要将title case应用于某个值,则Apache Commons WordUtils将为此提供现成的功能。

测试与规格

问题

您想编写使用Squeryl和Record访问数据库模型的Specs2单元测试。

使用内存数据库,并安排在测试之前进行设置,并在之后销毁。

这有三个部分:包括项目中的数据库并以内存模式连接到它; 创建可重用的特征来设置数据库; 然后在测试中使用trait。

H2数据库具有内存模式,这意味着它不会将数据保存到磁盘。它需要作为依赖关系包含在build.sbt中。在编辑build.sbt的同时,也禁用SBT的并行测试执行,以防止数据库测试相互影响

libraryDependencies += "com.h2database" % "h2" % "1.3.170"

parallelExecution in Test := false

创建一个特征来初始化数据库并创建模式

package code.model

import java.sql.DriverManager

import org.squeryl.Session
import org.squeryl.adapters.H2Adapter

import net.liftweb.util.StringHelpers
import net.liftweb.common._
import net.liftweb.http.{S, Req, LiftSession }
import net.liftweb.squerylrecord.SquerylRecord
import net.liftweb.squerylrecord.RecordTypeMode._

import org.specs2.mutable.Around
import org.specs2.execute.Result

trait TestLiftSession {
  def session = new LiftSession("", StringHelpers.randomString(20), Empty)
  def inSession[T](a: => T): T = S.init(Req.nil, session) { a }
}

trait DBTestKit extends Loggable {

  Class.forName("org.h2.Driver")

  Logger.setup = Full(net.liftweb.util.LoggingAutoConfigurer())
  Logger.setup.foreach { _.apply() }

  def configureH2() = {
    SquerylRecord.initWithSquerylSession(
      Session.create(
        DriverManager.getConnection("jdbc:h2:mem:dbname;DB_CLOSE_DELAY=-1",
        "sa", ""),
        new H2Adapter)
    )
  }

  def createDb() {
    inTransaction {
      try {
        MySchema.drop
        MySchema.create
      } catch {
        case e : Throwable =>
          logger.error("DB Schema error", e)
          throw e
      }
    }
  }

}

case class InMemoryDB() extends Around with DBTestKit with TestLiftSession {
  def around[T <% Result](testToRun: =>T) = {
    configureH2
    createDb
    inSession {
      inTransaction {
        testToRun
      }
    }
  }
}

总而言之,此跟踪提供了Specs2 InMemoryDB 上下文此上下文确保数据库已配置,创建模式,并在测试周围提供事务。

最后,将特征混合到测试中并在InMemoryDB上下文的范围内执行

例如,使用“一对多关系”的模式,我们可以测试星火星有两个卫星:

package code.model

import org.specs2.mutable._
import net.liftweb.squerylrecord.RecordTypeMode._
import MySchema._

class PlanetsSpec extends Specification with DBTestKit {

  sequential
  "Planets" >> {

    "know that Mars has two moons" >> InMemoryDB() {

      val mars = planets.insert(Planet.createRecord.name("Mars"))
      Satellite.createRecord.name("Phobos").planetId(mars.idField.is).save
      Satellite.createRecord.name("Deimos").planetId(mars.idField.is).save

      mars.satellites.size must_== 2
    }

  }
}

使用SBT的test命令运行它将显示成功:

> test
[info] PlanetsSpec
[info]
[info] Planets
[info] + know that Mars has two moons
[info]
[info]
[info] Total for specification PlanetsSpec
[info] Finished in 1 second, 274 ms
[info] 1 example, 0 failure, 0 error
[info]
[info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0
[success] Total time: 3 s, completed 03-Feb-2013 11:31:16

讨论

DBTestKit特性有做了很多工作,为我们的。在最底层,它加载H2驱动程序,并使用内存连接配置Squeryl。memJDBC连接字符串(jdbc:h2:mem:dbname;DB_CLOSE_DELAY=-1一部分意味着H2不会尝试将数据保留到磁盘。数据库只是驻留在内存中,所以磁盘中没有文件进行维护,并且运行速度很快。

默认情况下,当连接关闭时,内存中的数据库将被破坏。在这个配方中,我们通过添加禁用了DB_CLOSE_DELAY=-1,如果我们想要的话,这将允许我们编写跨连接的单元测试。

连接管理的下一步是在内存中创建数据库模式。我们这样做是createDb通过在开始测试时抛出模式和任何数据,并重新创建它。如果您有非常通用的测试数据集,那么在测试运行之前,这可能是插入该数据的好地方。

这些步骤在InMemoryDB中汇集在一起​​,该类实现了用于运行Around测试的代码的Specs2接口我们也围绕着一个测试TestLiftSession这提供了一个空的会话,如果您访问状态相关的代码(如S对象),这是非常有用的没有必要对Record和Squeryl进行测试,但是它已被包含在这里,因为你可能想在某些时候做到这一点。

在我们的规范本身中,我们混合使用访问数据库的测试DBTestKitInMemoryDB上下文。你会注意到,我们使用>>,而不是Specs2的shouldin你可能其他地方看到。这是为了避免您可能遇到的Specs2和Squeryl之间的名称冲突。

当我们禁用与SBT的并行执行时,我们也禁止在Specs2中执行并行执行sequential我们正在这样做,以防止一个测试可能期望另一个测试正在同时修改的数据的情况。

如果规范中的所有测试都将使用数据库,则可以使用Specs2 AroundContextExample[T]避免InMemoryDB在每次测试时都必须提及要做到这一点,混合AroundContextExample[InMemoryDB]并定义aroundContext

package code.model

import MySchema._

import org.specs2.mutable._
import org.specs2.specification.AroundContextExample
import net.liftweb.squerylrecord.RecordTypeMode._

class AlternativePlanetsSpec extends Specification with
  AroundContextExample[InMemoryDB] {

  sequential

  def aroundContext = new InMemoryDB()

  "Solar System" >> {

    "know that Mars has two moons" >> {

      val mars = planets.insert(Planet.createRecord.name("Mars"))
      Satellite.createRecord.name("Phobos").planetId(mars.idField.is).save
      Satellite.createRecord.name("Deimos").planetId(mars.idField.is).save

      mars.satellites.size must_== 2
    }
  }
}

AlternativePlanetsSpec现在所有的测试都会在InMemoryDB周围运行

我们使用了具有内存模式的数据库,以获得速度优势,无需文件清理。但是,您可以使用任何常规数据库:您需要更改驱动程序和连接字符串。

也可以看看

有关H2的内存数据库设置的更多信息,请参阅H2数据库网站

“MongoDB的单元测试记录”讨论了使用MongoDB的单元测试,但是对于SBT的其他测试命令和IDE中的测试的评论也将适用于该配方。

在列中存储随机值

问题

您需要一列来保存一个随机值。

使用UniqueIdField

import net.liftweb.record.field.UniqueIdField
val randomId = new UniqueIdField(this, 32) {}

注意{}在示例中; 这是UniqueIdField一个抽象类是必需的

大小值32表示要创建多少个随机字符。

讨论

UniqueIdField字段是一种,字段StringField的默认值来自StringHelpers.randomString该值是随机生成的,但不能保证在数据库中是唯一的。

UniqueIdField在该配方中支持的数据库列将是一个varchar(32) not null或类似的。存储的值将如下所示:

GOJFGQRLS5GVYGPH3L3HRNXTATG3RM5M

由于该值仅由字母和数字组成,因此可以方便地在URL中使用,因为没有字符可以转义。例如,它可以在链接中使用,以允许用户在通过电子邮件发送链接时验证其帐户,这是其中的用途之一ProtoUser

如果需要更改值,则reset该字段上方法将为该字段生成一个新的随机字符串。

如果您需要每行更有可能是唯一的自动值,则可以添加包含普遍唯一标识符(UUID)的字段

import java.util.UUID

val uuid = new StringField(this, 36) {
  override def defaultValue = UUID.randomUUID().toString
}

这将在您创建的记录中自动插入表单“6481a844-460a-a4e0-9191-c808e3051519”的值。

也可以看看

Java的UUID支持包括一个链接到RFC 4122,它定义了UUID。

自动创建和更新时间戳

问题

您希望在记录上创建和更新时间戳,并希望在添加或更新行时自动更新。

定义以下特征:

package code.model

import java.util.Calendar

import net.liftweb.record.field.DateTimeField
import net.liftweb.record.Record

trait Created[T <: Created[T]] extends Record[T] {
  self: T =>
  val created: DateTimeField[T] = new DateTimeField(this) {
    override def defaultValue = Calendar.getInstance
  }
}

trait Updated[T <: Updated[T]] extends Record[T] {
  self: T =>

  val updated = new DateTimeField(this) {
    override def defaultValue = Calendar.getInstance
  }

  def onUpdate = this.updated(Calendar.getInstance)

}

trait CreatedUpdated[T <: Updated[T] with Created[T]] extends
  Updated[T] with Created[T] {
    self: T =>
}

将特征添加到模型中。例如,我们可以修改Planet记录以包括创建和更新记录的时间:

class Planet private () extends Record[Planet]
  with KeyedRecord[Long] with CreatedUpdated[Planet] {
    override def meta = Planet
    // field entries as normal...
}

最后,安排该updated领域进行更新:

class MySchema extends Schema {
  ...
  override def callbacks = Seq(
    beforeUpdate[Planet] call {_.onUpdate}
  )
  ...

讨论

虽然有一个内置的net.liftweb.record.LifecycleCallbacks 特性,可以让你触发行为onUpdateafterDelete等,它仅适用于个别领域的应用,而不是记录。由于我们的目标是updated在记录的任何部分更改时更新该字段,所以我们不能使用LiftcycleCallbacks这里。

相反,CreatedUpdatedtrait简化了对记录的添加updated和 created字段,但是我们确实需要记住在模式中添加一个钩子,以确保在updated修改记录时改变值。这就是为什么我们设计callbacks了Schema。

CreatedUpdated混合记录的模式将包括两个附加列:

updated timestamp not null,
created timestamp not null

timestamp用于H2数据库。对于其他数据库,类型可能不同。

可以像任何其他记录字段一样访问这些值。使用“一对多关系”中的示例数据,我们可以运行以下操作:

val updated : Calendar = mars.updated.id
val created : Calendar = mars.created.is

如果您只需要创建时间或更新时间,只需混合使用Created[T]Updated[T]特征即可CreatedUpdated[T]

应该注意的onUpdate是,仅在完全更新时才调用,而不是使用Squeryl 进行部分更新完整更新是当对象被更改然后保存时; 部分更新是您尝试通过查询更改对象的位置。

如果您对Record的其他自动化感兴趣,则Squeryl模式回调支持这些触发的行为:

  • beforeInsert 和 afterInsert
  • afterSelect
  • beforeUpdate 和 afterUpdate
  • beforeDelete 和 afterDelete

也可以看看

完整和部分更新在插入,更新和删除中描述

记录SQL

问题

你想看到Squeryl执行的SQL。

在您有Squeryl季节之后添加以下内容,例如在您的查询之前:

org.squeryl.Session.currentSession.setLogger( s => println(s) )

通过提供一个String => Unit函数setLogger,Squeryl将使用它运行的SQL执行该函数。在这个例子中,我们只是将SQL打印到控制台。

讨论

您可能希望使用Lift中的记录功能来捕获SQL。例如:

package code.snippet

import net.liftweb.common.Loggable
import org.squeryl.Session

class MySnippet extends Loggable {

  def render = {
    Session.currentSession.setLogger( s => logger.info(s) )
    // ...your snippet code here...
  }
}

这将根据日志记录系统的设置(通常是在src / resources / props / default.logback.xml中配置的Logback项目)来记录查询

在开发过程中必须启用每个代码片段的记录可能是不方便的。要触发所有代码段的日志记录,您可以修改Boot.scala中addAround调用“配置Squeryl和记录”)以包括一个调用setLoggerinTransaction

S.addAround(new LoanWrapper {
  override def apply[T](f: => T): T = {
    val result = inTransaction {
    Session.currentSession.setLogger( s => logger.info(s) )
    // ... rest of addAround as normal

也可以看看

您可以从Logging wiki页面了解如何登录Lift 

使用MySQL MEDIUMTEXT为列建模

问题

你想使用MySQL的MEDIUMTEXT列,但StringField 没有这个选项。

dbType在你的架构中使用Squeryl

object MySchema extends Schema {
  on(mytable)(t => declare(
    t.mycolumn defineAs dbType("MEDIUMTEXT")
  ))
}

此架构设置将为您提供MySQL中正确的列类型:

create table mytable (
    mycolumn MEDIUMTEXT not null
);

在记录中,您可以StringField照常使用

讨论

该配方指出Squeryl的架构定义DSL可用的灵活性。此示例中的列属性只是您可以对Squeryl使用的默认选项进行的各种调整之一。

例如,您可以使用语法链接单个列的列属性,并同时定义多个列:

object MySchema extends Schema {
  on(mytable)(t => declare(
    t.mycolumn defineAs(dbType("MEDIUMTEXT"),indexed),
    t.id definedAs(unique, named("MY_ID"))
  ))
}

也可以看看

Squeryl的架构定义页面提供了可应用于表和列的属性示例。

MySQL字符集编码

问题

存储在MySQL数据库中的一些字符显示为???

确保这件事:

  • LiftRules.early.append(_.setCharacterEncoding("UTF-8"))包含在Boot.scala中
  • ?useUnicode=true&characterEncoding=UTF-8 包含在您的JDBC连接URL中。
  • 您的MySQL数据库已使用UTF-8字符集创建。

讨论

这里有许多可以影响MySQL数据库进入和出来的角色的交互。基本的问题是跨网络传输的字节没有意义,除非你知道编码。

Boot.scalasetCharacterEncoding("UTF-8")调用正在应用于servlet容器中的每个应用程序这是servlet容器接收到的请求中的参数是如何解释的。HTTPRequestServletRequest

另一方面,来自Lift的响应被编码为UTF-8。你会在很多地方看到这个。例如,templates-hidden/default包括:

<meta http-equiv="content-type" content="text/html; charset=UTF-8" />

此外,LiftResponse类将编码设置为UTF-8。

另一方面是来自Lift的字符数据如何通过网络发送到数据库。这由JDBC驱动程序的参数控制。MySQL的默认是检测编码,但从经验来看,这不是一个很好的选择,所以我们强制使用UTF-8编码。

最后,MySQL数据库本身需要将数据存储为UTF-8。默认字符编码不是UTF-8,因此在创建数据库时需要指定编码:

CREATE DATABASE myDb CHARACTER SET utf8

也可以看看

MySQL JDBC配置指南

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值