Rails7的connection.select_all对它的参数要求比较严格,向后不兼容。TypeError。不能铸造数组

158 阅读2分钟

我有一些代码想对ActiveRecord数据库执行一些原始SQL。这是一个复杂而奇怪的多表SQL(涉及一个postgres的递归CTE),所以基于特定模型的API似乎都不适合用于指定SQL。 它还需要接受一些参数,这些参数需要被适当地转义/消毒。

在某种程度上,我认为正确的方法是使用Model.connection.select_all ,这将创建一个参数化的准备语句。

我说的对吗?是否有更好的方法来做到这一点?该方法在Rails指南中被简要提及(展示了它是公共API!),但没有关于参数的很多细节。它的API文档非常有限,只是文档中提到。 **select_all**(arel, name = nil, binds = [], preparable: nil, async: false),"返回一个ActiveRecord::Result实例。" 没有解释参数的类型或语义。

在我之前的Rails 7上工作的代码中,调用看起来像。

MyModel.connection.select_all(
  "select complicated_stuff WHERE something = $1",
  "my_complicated_stuff_name",
  [[nil, value_for_dollar_one_sub]],
  preparable: true
)

  • 是的,binds 的值很奇怪,是一个数组中的二重数组,其中二重数组的第一个值是nil?这在任何地方都没有记录,我可能是从某个地方得到的......也许是StackOverflow的几个答案之一。
  • 老实说,我不知道preparable: true ,也不知道它有什么区别。

在Rails 7.0中,这开始失败了,出现了错误。TypeError: can't cast Array.

我根本找不到那个select_all all方法的任何文档,也找不到关于这个问题的其他讨论;我在Rails Changelog 中也找不到提到的任何select_all 的变化 我试着去看实际的代码历史,但是迷失了方向。我猜测 "不能铸造数组 "是指那个奇怪的binds 值......但它应该是什么?

最后我想到了寻找这个方法的Rails测试,这些测试使用了binds 这个参数,并最终成功地找到了一个!

所以......好吧,我用新的binds 参数重写了这个方法,像这样。

bind = ActiveRecord::Relation::QueryAttribute.new(
  "something", 
  value_for_dollar_one_sub, 
  ActiveRecord::Type::Value.new
)

MyModel.connection.select_all(
  "select complicated_stuff WHERE something = $1",
  "my_complicated_stuff_name",
  [bind],
  preparable: true
)

  • 确认这不仅在Rails 7中有效,而且一直到Rails 5.2都没有问题。
  • 我想我之前的做法是一些传统的传递参数的方式,在Rails 7中最终被删除了?
  • 我还是不明白我在做什么。ActiveRecord::Relation::QueryAttribute.new 的第一个参数,我让它与要进行比较的SQL列相匹配,但我不知道这是否重要,或者它是否有什么用处。第三个参数似乎是一个ActiveRecord类型......我只是把它留在通用的ActiveRecord::Type::Value.new ,这似乎对整数或字符串值都很好,不知道在什么情况下你想在这里使用一个特定的类型值,或者它有什么作用。
  • 总的来说,我想知道是否有更好的方法让我做我正在做的事情?我觉得很奇怪,在互联网上找不到其他人遇到过这种情况......尽管有stackoverflow的答案建议采用这种方法......也许我做错了?

但无论如何,由于这个问题很难调试,在谷歌上也很难找到文档或解释,而且我发现在Rails 7中根本没有提到这个问题的变化/破坏......我想我应该把它写出来,这样别人就有机会找到这个答案了。