考虑像这样的存储库/ DAO方法,效果很好:
def countReports(customerId: Long, createdSince: ZonedDateTime) =
DB.withConnection {
implicit c =>
SQL"""SELECT COUNT(*)
FROM report
WHERE customer_id = $customerId
AND created >= $createdSince
""".as(scalar[Int].single)
}
但是,如果使用可选参数:
定义方法,该怎么办?def countReports(customerId: Option[Long], createdSince: Option[ZonedDateTime])
指出,如果存在任一可选参数,则在过滤结果时使用它(如上所示),否则(如果它是None
)只是省略相应的WHERE条件。
使用可选的WHERE条件编写此方法的最简单方法是什么?作为Anorm新手,我很难找到一个这样的例子,但我想必须有一些这是明智的做法(也就是说,没有为每个现有/缺失参数组合重复SQL)。
请注意,java.time.ZonedDateTime
实例在Anorm timestamptz
调用中使用时,会完美地自动映射到Postgres SQL
。 (尝试将WHERE条件作为字符串提取,在SQL
之外,使用普通字符串插值创建不起作用; toString
生成数据库无法理解的表示。)
播放2.4.4
答案 0 :(得分:3)
一种方法是设置过滤子句,例如
val customerClause =
if (customerId.isEmpty) ""
else " and customer_id={customerId}"
然后将这些替换为SQL:
SQL(s"""
select count(*)
from report
where true
$customerClause
$createdClause
""")
.on('customerId -> customerId,
'createdSince -> createdSince)
.as(scalar[Int].singleOpt).getOrElse(0)
使用{variable}
而不是$variable
我认为更好,因为它可以降低SQL注入攻击的风险,因为有人可能会使用恶意字符串调用您的方法。如果您有其他符号未在SQL中引用(即子句字符串为空),Anorm不介意。最后,根据数据库(?),计数可能不返回任何行,因此我使用singleOpt而不是单个。
我很好奇你收到的其他答案。
编辑:Anorm插值(即SQL“...”,Scala的s“......”之外的插值实现,f“...”和原始“......”)was introduced允许使用$variable
与{variable}
同等地使用.on
。从Play 2.4开始,Scala和Anorm插值可以使用$
混合用于Anorm(SQL参数/变量)和#$
用于Scala(纯字符串)。事实上,只要Scala插值字符串不包含对SQL参数的引用,这确实很有效。唯一的方法,在2.4.4中,我发现在使用Anorm插值时在Scala插值字符串中使用变量是:
val limitClause = if (nameFilter="") "" else s"where name>'$nameFilter'"
SQL"select * from tab #$limitClause order by name"
但这很容易受到SQL注入的影响(例如像it's
这样的字符串会导致运行时语法异常)。因此,对于插值字符串中的变量,似乎有必要使用仅使用Scala插值的“传统”.on
方法:
val limitClause = if (nameFilter="") "" else "where name>{nameFilter}"
SQL(s"select * from tab $limitClause order by name").on('limitClause -> limitClause)
将来可能会扩展Anorm插值以解析变量的插值字符串?
Edit2:我发现有些表可能会不时更改查询中可能包含或不包含的属性数。对于这些情况,我正在定义一个上下文类,例如CustomerContext
。在这种情况下,对于影响sql的不同子句,有lazy val
个。 sql方法的调用者必须提供CustomerContext
,然后sql将包含${context.createdClause}
之类的内容,依此类推。这有助于提供一致性,因为我最终在其他地方使用上下文(例如分页的总记录数等)。
答案 1 :(得分:1)
最后让我的 simpler approach posted by Joel Arnold 在我的示例案例中工作,同时使用ZonedDateTime!
def countReports(customerId: Option[Long], createdSince: Option[ZonedDateTime]) =
DB.withConnection {
implicit c =>
SQL( """
SELECT count(*) FROM report
WHERE ({customerId} is null or customer_id = {customerId})
AND ({created}::timestamptz is null or created >= {created})
""")
.on('customerId -> customerId, 'created -> createdSince)
.as(scalar[Int].singleOpt).getOrElse(0)
}
棘手的部分是必须在空检查中使用{created}::timestamptz
。作为Joel commented,需要解决PostgreSQL driver issue问题。
显然,只有时间戳类型需要强制转换,而更简单的方式({customerId} is null
)可以与其他所有内容一起使用。另外,如果你知道其他数据库是否需要这样的东西,或者这只是Postgres的特性,请发表评论。
(虽然wwkudu's approach也可以正常工作,但这绝对更清晰,正如您在完整示例中看到comparing them side to side一样。)