Scala JDBC中可能存在的错误?

Scala JDBC中可能存在的错误?,scala,playframework,anorm,Scala,Playframework,Anorm,我现在面临一个奇怪的问题。 每当用户在搜索栏中键入以“s”开头的内容时,请求就会崩溃。 接下来看到的是我为这个项目编写的搜索引擎生成的示例sql代码 选择Profiles.ProfileID,Profiles.昵称,Profiles.Email,Profiles.Status,Profiles.Role,Profiles.Credits,Profiles.Language,Profiles.Created,Profiles.Modified,Profiles.Cover,Profiles.Pre

我现在面临一个奇怪的问题。 每当用户在搜索栏中键入以“s”开头的内容时,请求就会崩溃。 接下来看到的是我为这个项目编写的搜索引擎生成的示例sql代码

选择Profiles.ProfileID,Profiles.昵称,Profiles.Email,Profiles.Status,Profiles.Role,Profiles.Credits,Profiles.Language,Profiles.Created,Profiles.Modified,Profiles.Cover,Profiles.Prename,Profiles.Lastname,Profiles.BirthDate,Profiles.Country,Profiles.City,Profiles.Phone,Profiles.Website,Profiles.Description,Profiles,coalesceScores.numcore,0作为numcore,coalesceScores.numcorer,0作为numcorer, 选择计数* 来自喜欢 使用CommentId加入注释 其中Comments.ProfileID=Profiles.ProfileID NumLikes, 选择计数* 来自喜欢 使用CommentId加入注释 其中Comments.ProfileID=Profiles.ProfileID/ 选择合并NullIfcount*、0、1 根据评论 其中Comments.ProfileID=Profiles.ProfileID AvgLikes,Movies.MovieID,Movies.Caption,电影,描述,电影,语言,电影,乡村,电影,城市,电影,善良,电影,融合, 选择castleast25+5.000000*round75*0.500000*SIZE/1024.0/1024.0*0.001250+0.500000*Duration/60.0*0.050000/5.000000,100作为签名整数 从溪流 其中MovieID=Movies.MovieID AND Tag=main 和编码=mp4作为ChargeMain, 选择castleast25+10.000000*round75*0.200000*SIZE/1024.0/1024.0*0.001000+0.800000*Duration/60.0*0.016667/10.000000,100作为签名整数 从溪流 其中MovieID=Movies.MovieID 和标记=注释 和编码=mp4作为ChargeNotes, 选择合并计数*,0 从观点 其中Views.MovieID=Movies.MovieID 和Tag=main作为主视图, 选择合并计数*,0 从观点 其中Views.MovieID=Movies.MovieID AND Tag=注释为注释视图, 选择合并计数*,0 从观点 其中Views.MovieID=Movies.MovieID 和标记=拖车作为拖车视图, 选择聚结 选择合并计数*,0 从观点 其中Views.MovieID=Movies.MovieID 标签=拖车, 选择合并计数*,0 从观点 其中Views.MovieID=Movies.MovieID 并且Tag=main,0作为MaxMainTrailerViews, 选择avgScore 从分数 其中Scores.MovieID=Movies.MovieID作为分数, 选择coalescegroup_concatcastScore作为带符号整数, 从分数 其中Scores.MovieID=Movies.MovieID作为分数,Movies.Cover,Movies.Locked,电影。制作,电影。修改, 选择合并组_concatname分隔符',', 来自标签 使用TagID连接标记链接 其中TagLinks.MovieID=Movies.MovieID 按名称订购ASC作为标签, 选择计数* 从购买 其中MovieID=Movies.MovieID 和ProfileID=%s 和类型=作为采购主设备的主设备, 选择计数* 从购买 其中MovieID=Movies.MovieID 和ProfileID=%s AND TYPE=注释为已购买的注释, 选择计数* 从观察名单 其中MovieID=Movies.MovieID 和ProfileID=%s作为观察列表, 选择计数* 从分数 其中MovieID=Movies.MovieID 和ProfileID=%s作为额定值, 选择计数* 根据评论 其中MovieID=Movies.MovieID 和Deleted作为注释为空, 选择sumDuration 从溪流 其中Streams.MovieID=Movies.MovieID 和流。标记在主, 笔记 和Streams.ENCODING=mp4作为运行时, 选择castcount*作为有符号整数 来自电影 在Profiles.ProfileI上连接配置文件 D=Movies.ProfileID WHERE Movies.Locked=0 和 选择计数* 从溪流 其中Streams.MovieID=Movies.MovieID 和流。状态就绪=0 和配置文件。状态=活动 或%s=1 或电影。配置文件ID=%s 作为电影,, 选择castceilcount*/%s作为签名整数 来自电影 使用ProfileID连接配置文件 WHERE Movies.Locked=0 和 选择计数* 从溪流 其中Streams.MovieID=Movies.MovieID 和流。状态就绪=0 和配置文件。状态=活动 或%s=1 或电影。配置文件ID=%s 作为页面 来自电影 使用ProfileID连接配置文件 左连接 选择Movies.ProfileID作为ProfileID, AvgScore。得分为AvgScore, 将*计算为NumScore, countDISTINCT Scores.ProfileID作为NumScorer 从分数 使用MovieId加入电影 按电影分组。使用ProfileID将ProfileID作为分数 WHERE Movies.Locked=0 和 选择计数* 从溪流 其中Streams.MovieID=Movies.MovieID 和流。状态就绪=0 和配置文件。状态=活动 或%s=1 或电影。配置文件ID=%s 按分数说明的订单限制%s, %

经过无数小时的调查和比较可能的用户输入和生成的SQL代码,我最终将问题归结为JDBC驱动程序的一些非常奇怪的行为,我认为这是一个严重的错误-但我不确定:

我又花了几个小时试图用尽可能少的sql代码重现这个问题,结果是:

SQL("""select * from Movies where "s" like "%s" and MovieID = {a} """)
.on('a -> 1).as(scalar[Long]*)
[SQLException:参数索引超出范围1>参数数,即0。]

SQL("""select * from Movies where "s" like "%samuel" and MovieID = {a} """)
.on('a -> 1).as(scalar[Long]*)
SQL("""select * from Movies where "s" like "%flower" and MovieID = {a} """)
.on('a -> 1).as(scalar[Long]*)
[SQLException:参数索引超出范围1>参数数,即0。]

SQL("""select * from Movies where "s" like "%samuel" and MovieID = {a} """)
.on('a -> 1).as(scalar[Long]*)
SQL("""select * from Movies where "s" like "%flower" and MovieID = {a} """)
.on('a -> 1).as(scalar[Long]*)
[好的]

[好的]

[好的]

[好的]

我相信在这里可以看到一种模式: 在确切的条件下,sql代码中的任何位置都有一个带引号或不带引号的%s序列,后跟一个具有任意名称和任意距离的不带引号的命名参数 对于%s序列,jdbc或anorm崩溃。崩溃似乎发生在JDBC中,但也可能是Anorm向JDBC提交了无效值


你们有什么建议吗?

这就是它现在的实施方式:

  /**
   * This private helper method transforms a content filter string into an sql expression
   * for searching within movies, owners and kinds and tags.
   * @author Samuel Lörtscher
   */
  private def contentFilterToSql(value: String) = {
    // trim and clean and the parametric value from any possible anomalies
    // (those include strange spacing and non closed quotes)
    val cleaned = value.trim match {
      case trimmed if trimmed.count(_ == '"') % 2 != 0 =>
        if (trimmed.last == '"') trimmed.dropRight(1).trim
        else trimmed + '"'
      case trimmed =>
        trimmed
    };

    // transform the cleaned value into a list of expressions
    // (words between quotes are considered being one expression)
    // empty expressions between quotes are being removed
    // expressions will contain no quotes as they are being stripped during evaluation -
    // thus counter measures for sql injection should be obsolete
    // (we put an empty space at the end because it makes the lexer algorithm much
    // more efficient as it will not need to check for end of file in every iteration)
    val expressions = (cleaned + " ").foldLeft((List[String](), "", false)) { case ((list, expr, quoted), char) =>
      // perform the lexer operation for the current character
      if (char == ' ' && !quoted) (expr :: list, "", false)
      else if (char == '"') (expr :: list, "", !quoted)
      else (list, expr + char, quoted)
    }._1.filter(_.nonEmpty).map(_.trim)

    // finally transform the expression
    // list into a variable length sql condition statement
    expressions.map { expr =>
      s"""
        (concat(Movies.Caption, " ", Movies.Description, " ", Movies.Kind, " ", Profiles.Nickname, " ",
        (select coalesce(group_concat(Tags.Name), "")
        from Tags join TagLinks using (TagID) where TagLinks.MovieID = Movies.MovieID)) like "%$expr%")
      """
    }.mkString(" and ")
  }
由于搜索表达式的数量是可变的,因此我不能在此处使用Anorm参数:-/

我现在找到了一个简单的解决方案,但我并不十分高兴被迫应用这些蹩脚的黑客。 由于放置%s字符序列似乎会触发错误,因此我正在寻找在不直接传递此字符序列的情况下提交相同语义结果的可能性。我最终用concat%,$expr%替换了like%$expr%。由于concat是在like之前由MySql服务器引擎进行评估的,因此他将在like处理之前将原始模式重新组合在一起,并且不会通过anorm、jdbc数据处理器传输序列%s

// finally transform the expression
// list into a variable length sql condition statement
// (freaking concat("%", "$expr%")) is required due to a freaking bug in either anorm or JDBC
// which results into a crash when %s is anyway submitted)
expressions.map { expr =>
  s"""
    (concat(Movies.Caption, " ", Movies.Description, " ", Movies.Kind, " ", Profiles.Nickname, " ",
    (select coalesce(group_concat(Tags.Name), "")
    from Tags join TagLinks using (TagID) where TagLinks.MovieID = Movies.MovieID)) like concat("%", "$expr%"))
  """
}.mkString(" and ")

我想我同时找到了解决这个问题的持久办法。因为我的sql生成器需要保持非常灵活,所以我不知何故需要一种方法来传递带有相应参数的sql片段,而无需立即对其进行计算。相反,生成器必须能够在任何时候将各种sql片段组合成更大的片段——就像他现在所做的那样——但现在有了一个公司的、尚未评估的参数。我提出了这个原型:

DB.withConnection("betterdating") { implicit connection =>
  case class SqlFragment(Fragment: String, Args: NamedParameter*)

  val aa = SqlFragment("select MovieID from Movies")
  val bb = SqlFragment("join Profiles using(ProfileID)")
  val cc = SqlFragment("where Caption like \"%{a}\" and MovieID = {b}", 'a -> "s", 'b -> 5)

  // combine all fragments
  val v1 = SQL(Seq(aa, bb, cc).map(_.Fragment).mkString(" "))
            .on((aa.Args ++ bb.Args ++ cc.Args): _*)

  // better solution
  val v2 = Seq(aa, bb, cc).unzip(frag => (frag.Fragment, frag.Args)) match {
    case (frags, args) => SQL(frags.mkString(" ")).on(args.flatten: _*)
  }

  // works
  println(v1.as(scalar[Long].singleOpt))
  println(v2.as(scalar[Long].singleOpt))
}
看起来效果不错-

然后我重写了freetext过滤器的最后一部分,如下所示:

// finally transform the expression
// list a single sql fragment
expressions.zipWithIndex.map { case (expr, index) =>
  s"""
    (concat(Movies.Caption, " ", Movies.Description, " ", Movies.Kind, " ", Profiles.Nickname, " ",
    (select coalesce(group_concat(Tags.Name), "") from Tags join TagLinks using (TagID)
    where TagLinks.MovieID = Movies.MovieID)) like "%{expr$index}%"))
  """ -> (s"expr$index" -> expr)
}.unzip match { case (frags, args) => SqlFragment(frags.mkString(" and "), args.flatten: _*)

您认为如何?

查询是针对mysql database serverFailing语句执行的,该语句包括序列%s,它是一个格式标识符,因此与标准字符串格式冲突。尝试用Anorm占位符替换语句中的占位符,并将值移到.on…,或使用Anorm字符串插值,例如。。。像${%s}。。。。作为参数传递在任何方面都更好,驱动程序正确地引用字符串。嗯,老实说,我不认为这样做有什么意义。Scala字符串插值是在处理Anorm或JDBC之前完成的,因此不会影响这些技术将接收的数据?错。Anorm interpolation使用驱动器将值作为JDBC参数传递,如.on.Related Anorm update:但您可以使用数量可变的参数绑定。这不是答案。如果您想添加一些代码,请编辑您的问题。如前所述,最好将字符串值作为参数传递,而不是将其包含在语句中,您尝试过吗?此外,对于多值参数,Anorm支持搜索表达式的数量是可变的。
// finally transform the expression
// list a single sql fragment
expressions.zipWithIndex.map { case (expr, index) =>
  s"""
    (concat(Movies.Caption, " ", Movies.Description, " ", Movies.Kind, " ", Profiles.Nickname, " ",
    (select coalesce(group_concat(Tags.Name), "") from Tags join TagLinks using (TagID)
    where TagLinks.MovieID = Movies.MovieID)) like "%{expr$index}%"))
  """ -> (s"expr$index" -> expr)
}.unzip match { case (frags, args) => SqlFragment(frags.mkString(" and "), args.flatten: _*)