Squeryl是一个对象关系映射库。它将Scala类转换为关系数据库中的表,行和列,并提供了一种编写由Scala编译器进行类型检查的类似SQL的查询的方法。Lift Squeryl Record模块将Squeryl与Record进行集成,这意味着Lift应用程序可以使用Squeryl来存储和获取数据,同时利用Record的功能,如数据验证。
本章的代码可以在https://github.com/LiftCookbook/cookbook_squeryl找到。
配置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 transaction
和inTransaction
函数中包装查询。这两个调用之间的区别在于,inTransaction
如果不存在,将会启动一个新的事务,而transaction
总是创建一个新的事务。
通过确保所有HTTP请求addAround
都可以进行交易,我们可以在Lift中编写查询,而在大多数情况下,不必自行建立交易,除非我们想要。例如:
import
net.liftweb.squerylrecord.RecordTypeMode._
val
r
=
myTable
.
insert
(
MyRecord
.
createRecord
.
myField
(
aValue
))
在这个食谱中,PostgreSqlAdapter
被使用。Squeryl还支持:OracleAdapter
,MySQLInnoDBAdapter
和MySQLAdapter
,MSSQLServer
,H2Adapter
,DB2Adapter
,和DerbyAdapter
。
使用JNDI DataSource
解
在Boot.scala,调用initWithSquerylSession
了DataSource
从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
)
)
用mydb
JNDI配置中的数据库名称替换,并替换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>
一对多关系
解
oneToManyRelation
在您的模式中使用Squeryl ,并在您的Lift模型中,包括从卫星到地球的参考。
目标是模拟关系,如图7-1所示。
![一颗行星可能有很多卫星,但一颗卫星只能绕着一颗星球](https://i-blog.csdnimg.cn/blog_migrate/cf9d1cad4760f04ad268f6a46e3b99a1.png)
代码:
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
基于(via
)planetId
在卫星表中建立。
这给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.left
和right
方法来建立查找查询为Planet.satellites
和Satellite.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
如果有机会找不到记录,那将是更安全的方式,因为它将评估到None
或Some[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
但是更新数据库 - 删除关系
assign
associate
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]
中assign
和delete
。请注意,要删除多对一的“一”一方,分配给记录的任何内容都将需要已经被删除,以避免由于例如离开参考不存在的行星的卫星而产生的数据库约束错误。
由于left
和right
是查询,这意味着每次使用它们时,你会被发送一个新的数据库查询。Squeryl将这些形式称为无国籍关系。
该状态的版本left
和right
这个样子:
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访问](https://i-blog.csdnimg.cn/blog_migrate/91b4a2d908fe56dab2f123243f3c64bc.png)
该模式根据两个表进行定义,一个用于行星,一个用于空间探测,另外还有两个基于第三个类之间的关系,称为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
[
Probe
,Visit
]
=
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
[
Planet
,Visit
]
=
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.probes
并Probe.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]
是这里的核心元件连接我们Planet
,Probe
和Visit
记录在一起。它允许我们访问了“左”和“右”的关系,双方在我们的模型Probe.planets
和Planet.probes
。
与一对多关系的“一对多关系”一样,左侧和右侧都是查询。当您要求时Planet.probes
,数据库将通过Visit
记录中的加入进行适当的查询:
Select
Probe
.
name
,
Probe
.
idField
From
Visit
,
Probe
Where
(
Visit
.
probeId
=
Probe
.
idField
)
and
(
Visit
.
planetId
=
?
)
同样如“一对多关系”中所述,有查询结果的状态变体left
和right
缓存。
在我们插入数据库的数据中,我们没有提及Visit
。Squeryl 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
仍然是一个容器planetId
和probeId
参考,但我们也有一个平均的整数持有人的访问年份。
记录访问年份,我们需要assign
提供的方法ManyToMany[T]
。这将建立关系,但不会更改数据库。相反,它返回实例Visit
,我们可以更改然后存储在数据库中:
probeVisits
.
insert
(
voyager1
.
planets
.
assign
(
saturn
).
year
(
1980
))
assign
在这种情况下Visit
,返回类型是,并Visit
具有一个year
字段。通过插入Visit
记录probeVisits
将在表中创建一行用于访问。
要访问Visit
对象上的这些额外信息,可以使用以下几种方法ManyToMany[T]
:
-
查询返回的
Visit
相关对象Planet.probes
或Probe.planets
-
一个查询返回的对
(Planet,Visit)
或(Probe,Visit)
根据您在其上调用的连接的哪一方(probes
或planets
)
associations
associationMap
例如,在代码片段中,我们可以列出所有的空间探测器,并且对于每个探测器,可以显示它访问的星球以及它在那一年的行星。该片段将如下所示:
"#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>
![使用此配方中的多对多功能的输出示例](https://i-blog.csdnimg.cn/blog_migrate/e0e24c04fb3ad145d1a83bdf167c7438.png)
要删除关联,使用dissociate
或dissociateAll
方法上left
或right
查询。删除单个关联:
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
=
?
你不能做的是删除一个Planet
或Probe
如果该记录在关系中仍然有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
;
向字段添加验证
解
覆盖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
}
自定义验证逻辑
解
从字段的类型实现一个函数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
}
在设置之前修改一个字段值
解
要删除用户输入的前导和尾随空格,该字段将使用trim
过滤器:
val
name
=
new
StringField
(
this
,
256
)
{
override
def
setFilter
=
trim
_
::
super
.
setFilter
}
讨论
内置的过滤器有:
- 通过截断强制字段的最小和最大长度
-
适用
String.trim
于字段值 - 更改字段值的大小写
- 删除匹配的正则表达式字符
- 将空值转换为空字符串
crop
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
类将为此提供现成的功能。
测试与规格
解
这有三个部分:包括项目中的数据库并以内存模式连接到它; 创建可重用的特征来设置数据库; 然后在测试中使用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。mem
JDBC连接字符串(jdbc:h2:mem:dbname;DB_CLOSE_DELAY=-1
)的一部分意味着H2不会尝试将数据保留到磁盘。数据库只是驻留在内存中,所以磁盘中没有文件进行维护,并且运行速度很快。
默认情况下,当连接关闭时,内存中的数据库将被破坏。在这个配方中,我们通过添加禁用了DB_CLOSE_DELAY=-1
,如果我们想要的话,这将允许我们编写跨连接的单元测试。
连接管理的下一步是在内存中创建数据库模式。我们这样做是createDb
通过在开始测试时抛出模式和任何数据,并重新创建它。如果您有非常通用的测试数据集,那么在测试运行之前,这可能是插入该数据的好地方。
这些步骤在InMemoryDB
类中汇集在一起,该类实现了用于运行Around
测试的代码的Specs2接口。我们也围绕着一个测试TestLiftSession
。这提供了一个空的会话,如果您访问状态相关的代码(如S
对象),这是非常有用的。没有必要对Record和Squeryl进行测试,但是它已被包含在这里,因为你可能想在某些时候做到这一点。
在我们的规范本身中,我们混合使用访问数据库的测试DBTestKit
的InMemoryDB
上下文。你会注意到,我们使用>>
,而不是Specs2的should
和in
你可能其他地方看到。这是为了避免您可能遇到的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
特性,可以让你触发行为onUpdate
,afterDelete
等,它仅适用于个别领域的应用,而不是记录。由于我们的目标是updated
在记录的任何部分更改时更新该字段,所以我们不能使用LiftcycleCallbacks
这里。
相反,CreatedUpdated
trait简化了对记录的添加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季节之后添加以下内容,例如在您的查询之前:
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和记录”)以包括一个调用:setLogger
inTransaction
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为列建模
解
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字符集编码
解
确保这件事:
LiftRules.early.append(_.setCharacterEncoding("UTF-8"))
包含在Boot.scala中。?useUnicode=true&characterEncoding=UTF-8
包含在您的JDBC连接URL中。- 您的MySQL数据库已使用UTF-8字符集创建。
讨论
这里有许多可以影响MySQL数据库进入和出来的角色的交互。基本的问题是跨网络传输的字节没有意义,除非你知道编码。
Boot.scala中的setCharacterEncoding("UTF-8")
调用正在应用于servlet容器中的每个应用程序。这是servlet容器接收到的请求中的参数是如何解释的。HTTPRequest
ServletRequest
另一方面,来自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