【问题标题】:"In" clause in anorm?异常中的“在”子句?
【发布时间】:2012-03-20 15:50:00
【问题描述】:

在 anorm 中使用“in”子句似乎并不容易:

val ids = List("111", "222", "333")
val users = SQL("select * from users where id in ({ids})").on('ids-> ???).as(parser *)

如何替换??? 部分?

我试过了:

on('ids -> ids)
on('ids -> ids.mkString("'","','","'"))
on('ids -> ids.mkString("','")

但没有一个有效。

我在讨论中看到完全相同的问题:https://groups.google.com/d/topic/play-framework/qls6dhhdayc/discussion,作者有一个复杂的解决方案:

val params = List(1, 2, 3) 

val paramsList = for ( i <- 0 until params.size ) yield ("userId" + i) 

// ---> results in List("userId0", "userId1", "userId2") 

User.find("id in ({%s})"

    // produces "id in ({userId0},{userId1},{userId2})"
    .format(paramsList.mkString("},{")) 

    // produces Map("userId0" -> 1, "userId1" -> 2, ...) 
    .on(paramsList.zip(params))
    .list() 

这太复杂了。

有没有更简单的方法?还是应该让游戏变得更容易?

【问题讨论】:

    标签: scala anorm


    【解决方案1】:

    自 2.3 起,Anorm 现在支持这种情况(以及更多):"Using multi-value parameter"

    回到它给出的初始示例:

    val ids = Seq("111", "222", "333")
    val users = SQL("select * from users where id in ({ids})").on('ids-> ids).as(parser *)
    

    【讨论】:

    • 也可以使用SQL"select * from users where id in ($ids)".as(parser.*)
    • 请注意,在 2.3 中,这需要明确定义为 Seq - list.toSeq 工作(参见 this)。
    • 请注意,2.3 已经很老了
    【解决方案2】:

    成功了!该线程实际上没有更多更新,但它似乎仍然相关。正因为如此,而且因为没有答案,我想我会考虑一下。

    Anorm 不支持“IN”子句。我怀疑他们永远不会。你无法让它们发挥作用,我什至读过一篇文章,其中 anorm 专门删除了这些子句,因为它们让 Anorm 感觉“像一个 ORM”。

    然而,将 SqlQuery 包装在一个支持 IN 子句的短类中,然后在需要时将该类转换为 SqlQuery 是相当容易的。

    这里没有粘贴代码,因为它有点长,这里是指向我的博客的链接,我在其中发布了代码以及如何使用它。

    In clause with Anorm

    基本上,当您获得我博客中的代码时,您的语句如下所示:

    RichSQL(""" SELECT * FROM users WHERE id IN ({userIds}) """).onList("userIds" -> userIds).toSQL.as(userParser *)(connection)
    

    【讨论】:

      【解决方案3】:

      也许为时已晚,但这里有一个使用自定义字符串插值的技巧,它也适用于解决 IN 子句的问题。

      我已经实现了一个帮助类来定义一个字符串插值。您可以在下面看到它,您可以简单地复制和粘贴,但首先让我们看看如何使用它。

      与其写东西

      SQL("select * from car where brand = {brand} and color = {color} and year = {year} order by name").on("brand" -> brand, "color" -> color, "year" -> year).as(Car.simple *)
      

      你可以简单地写:

      SQL"select * from car where brand = $brand and color = $color and year = $year order by name".as(Car.simple *)
      

      所以使用字符串插值更简洁易读。

      而对于使用IN子句的情况,可以这样写:

      val carIds = List(1, 3, 5)
      SQLin"select * from car where id in ($carIds)".as(Car.simple *)
      

      或者你的例子:

      val ids = List("111", "222", "333")
      val users = SQLin"select * from users where id in ($ids)".as(parser *)
      

      有关字符串插值的更多信息,请查看link

      这个隐式类的代码如下:

      package utils
      
      object AnormHelpers {
      
        def wild (str: String) = "%" + str + "%"
      
        implicit class AnormHelper (val sc: StringContext) extends AnyVal {
      
          // SQL raw -> it simply create an anorm.Sql using string interpolation
          def SQLr (args: Any*) = {
            // Matches every argument to an arbitrary name -> ("p0", value0), ("p1", value1), ...
            val params = args.zipWithIndex.map(p => ("p"+p._2, p._1))
            // Regenerates the original query substituting each argument by its name with the brackets -> "select * from user where id = {p0}"
            val query = (sc.parts zip params).map{ case (s, p) => s + "{"+p._1+"}" }.mkString("") + sc.parts.last
            // Creates the anorm.Sql
            anorm.SQL(query).on( params.map(p => (p._1, anorm.toParameterValue(p._2))) :_*)
          }
      
          // SQL -> similar to SQLr but trimming any string value
          def SQL (args: Any*) = {
            val params = args.zipWithIndex.map {
              case (arg: String, index) => ("p"+index, arg.trim.replaceAll("\\s{2,}", " "))
              case (arg, index) => ("p"+index, arg)
            } 
            val query = (sc.parts zip params).map { case (s, p) => s + "{"+ p._1 + "}" }.mkString("") + sc.parts.last
            anorm.SQL(query).on( params.map(p => (p._1, anorm.toParameterValue(p._2))) :_*)
          }
      
          // SQL in clause -> similar to SQL but expanding Seq[Any] values separated by commas
          def SQLin (args: Any*) = {
            // Matches every argument to an arbitrary name -> ("p0", value0), ("p1", value1), ...
            val params = args.zipWithIndex.map {
              case (arg: String, index) => ("p"+index, arg.trim.replaceAll("\\s{2,}", " "))
              case (arg, index) => ("p"+index, arg)
            }
            // Expands the Seq[Any] values with their names -> ("p0", v0), ("p1_0", v1_item0), ("p1_1", v1_item1), ...
            val onParams = params.flatMap {
              case (name, values: Seq[Any]) => values.zipWithIndex.map(v => (name+"_"+v._2, anorm.toParameterValue(v._1)))
              case (name, value) => List((name, anorm.toParameterValue(value)))
            }
            // Regenerates the original query substituting each argument by its name expanding Seq[Any] values separated by commas
            val query = (sc.parts zip params).map {
              case (s, (name, values: Seq[Any])) => s + values.indices.map(name+"_"+_).mkString("{", "},{", "}")
              case (s, (name, value)) => s + "{"+name+"}"
            }.mkString("") + sc.parts.last
            // Creates the anorm.Sql
            anorm.SQL(query).on(onParams:_*)
          }
        }
      
      }
      

      【讨论】:

      • 回答这个问题可能为时已晚,但它正好及时帮助了我。完美运行
      【解决方案4】:

      可能已经晚了,但我为其他寻找相同的人添加了这个。 您可以使用一些内置的数据库功能来克服这个问题。这是 Anorm 相对于 ORM 的优势之一。例如,如果您使用的是 PostgreSQL,您可以将列表作为数组传递,并在查询中取消嵌套该数组:

      我假设 id 是整数。

      val ids = List(1, 2, 3)
      
      val idsPgArray = "{%s}".format(ids.mkString(",")) //Outputs {1, 2, 3}
      
      val users = SQL(
        """select * from users where id in (select unnest({idsPgArray}::integer[]))"""
      ).on('ids-> ???).as(parser *)
      

      执行的查询将是

      select * from users where id in (select unnest('{1, 2, 3}'::integer[])) 
      

      等于

      select * from users where id in (1, 2, 3)
      

      【讨论】:

        【解决方案5】:

        我最近遇到了同样的问题。不幸的是,似乎没有办法不使用字符串插值,因此容易受到 SQL 注入的影响。

        我最终做的是通过将其转换为整数列表并返回来对其进行清理:

        val input = "1,2,3,4,5"
        
        // here there will be an exception if someone is trying to sql-inject you
        val list = (_ids.split(",") map Integer.parseInt).toList
        
        // re-create the "in" string
        SQL("select * from foo where foo.id in (%s)" format list.mkString(","))
        

        【讨论】:

          【解决方案6】:
          User.find("id in (%s)"
            .format(params.map("'%s'".format(_)).mkString(",") )
            .list() 
          

          【讨论】:

          • 此解决方案实际上与问题中建议的解决方案相同,唯一的例外是它容易受到 SQL 注入的影响,因为您将参数直接粘贴到查询字符串中(没有转义!),而不是使用绑定参数。为了节省14个字符,不值得!
          • 一个巨大的安全漏洞不值 14 个字符?!
          【解决方案7】:
          val ids = List("111", "222", "333")
          val users = SQL("select * from users 
                           where id in 
                           (" +  ids.reduceLeft((acc, s) => acc + "," + s) + ")").as(parser *)
          

          【讨论】:

          • reduceLeft 部分写成简单的ids.mkString(",") 会更符合习惯。根据另一个答案,由于将参数直接放入查询字符串,这很容易受到 SQL 注入的影响。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-02-12
          • 2021-09-09
          • 1970-01-01
          相关资源
          最近更新 更多