Anorm:WHERE条件,有条件的

Anorm: WHERE condition, conditionally

考虑像这样的 repository/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 新手,我一直在努力寻找这样的示例,但我想一定有 一些 明智的方法来做到这一点(即,不为 present/missing 参数的每个组合复制 SQL)。

请注意,当在 Anorm SQL 调用中使用时,java.time.ZonedDateTime 实例会完美且自动地映射到 Postgres timestamptz。 (尝试将 WHERE 条件提取为字符串,在 SQL 之外,使用正常的字符串插值创建没有成功;toString 产生了数据库无法理解的表示。)

播放 2.4.4

一种方法是设置过滤子句,例如

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 注入攻击的风险,在这种情况下,有人可能会使用恶意字符串调用您的方法。 Anorm 不介意您是否有未在 SQL 中引用的其他符号(即如果子句字符串为空)。最后,根据数据库(?),计数可能 return 没有行,所以我使用 singleOpt 而不是 single.

我很好奇您还收到了什么其他答案。

编辑:Anorm 插值(即 SQL"...",一种超越 Scala 的 s"..."、f"..." 和 raw"..." 的插值实现)was introduced 允许使用 $variable 等同于 {variable}.on。从 Play 2.4 开始,Scala 和 Anorm 插值可以混合使用 $ 用于 Anorm(SQL parameter/variable)和 #$ 用于 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 这样的字符串会导致运行时语法异常)。因此,对于内插字符串中的变量,似乎有必要使用 "traditional" .on 方法,仅使用 Scala 插值:

val limitClause = if (nameFilter="") "" else "where name>{nameFilter}"
SQL(s"select * from tab $limitClause order by name").on('limitClause -> limitClause)

也许将来可以扩展 Anorm 插值以解析变量的插值字符串?

Edit2:我发现有些表中可能包含或可能不包含在查询中的属性数量不时发生变化。对于这些情况,我定义了一个上下文 class,例如CustomerContext。在这种情况下 class 有 lazy val 用于影响 sql 的不同子句。 sql 方法的调用者必须提供一个 CustomerContext,然后 sql 将包含诸如 ${context.createdClause} 等。这有助于提供一致性,因为我最终在其他地方使用上下文(例如分页的总记录数等)。

终于让这个 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)
  }

棘手的部分是必须在 null 检查中使用 {created}::timestamptz。如Joel commented, this is needed to work around a PostgreSQL driver issue

显然只有 timestamp 类型才需要转换,而更简单的方法 ({customerId} is null) 适用于其他所有类型。另外,如果您知道其他数据库是否需要这样的东西,或者这是 Postgres-only 的特性,请发表评论。

(在完整示例中为 also works fine, this definitely is cleaner, as you can see comparing them side to side。)