Anorm:WHERE条件,有条件

时间:2015-12-04 00:38:03

标签: scala playframework playframework-2.4 anorm

考虑像这样的存储库/ 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

2 个答案:

答案 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一样。)