Anorm:WHERE 条件,有条件

Posted

技术标签:

【中文标题】Anorm:WHERE 条件,有条件【英文标题】:Anorm: WHERE condition, conditionally 【发布时间】:2015-12-04 00:38:02 【问题描述】:

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

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

播放 2.4.4

【问题讨论】:

我在GitHub test repo 玩过不同的选项;随意分叉。 【参考方案1】:

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

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 中未引用的其他符号(即,如果子句字符串为空)。最后,根据数据库(?),计数可能不会返回任何行,所以我使用 singleOpt 而不是 single。

我很好奇您还收到了哪些其他答案。

编辑:异常插值(即 SQL“...”,一种超越 Scala 的 s“...”、f“...”和原始“...”的插值实现)was introduced 允许使用 @ 987654327@ 等同于 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。在这种情况下,类有lazy vals 用于影响sql 的不同子句。 sql 方法的调用者必须提供CustomerContext,然后sql 将包含$context.createdClause 等。这有助于保持一致性,因为我最终会在其他地方使用上下文(例如分页的总记录数等)。

【讨论】:

使用 Anorm 插值SQL"..."(不是标准的字符串插值,并且以相同的方式设置参数值)不再存在 SQL 注入风险 谢谢,这种方法很干净,而且确实有效!我最初错过的是您使用的是SQL(""" """) 而不是SQL""" """。 These are NOT the same,在与 ERROR: syntax error at or near "" 斗争了一段时间后,我学到了艰难的方法。您不能在 SQL""" """ 中使用 customerId 语法。 @cchantep:对于这种“条件条件”案例,对我来说,我似乎需要customerId.on() 方法来让它工作。如果您愿意,可以通过发布一个等效但使用SQL"..." 实现的答案来证明我错了:) (当然我的意思是上面的SQL(s""" """) - s 是正确替换$customerClause 等所必需的。) 有趣 - 我没有见过 SQL "..." 格式。谢谢。【参考方案2】:

终于让这个 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 所必需的。

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

(虽然wwkudu's approach 也可以正常工作,但这绝对更干净,您可以在完整示例中看到comparing them side to side。)

【讨论】:

以上是关于Anorm:WHERE 条件,有条件的主要内容,如果未能解决你的问题,请参考以下文章

SQL语句中连接on条件和where条件的执行顺序

SQL语句中where条件的写法

如何知道 WHERE 语句中有多少条件满足其条件?

sqlwhere后可以有多个条件后再嵌套吗

SQL语句where多条件查询怎么写

sqlor后面条件生效了,前面的where没用了