如何在保持双引号的同时连接数组中的元素?

时间:2015-08-03 04:02:25

标签: ruby-on-rails ruby sqlite

我正在阅读Rebuilding Rails一书。在小ORM章节中,它使用sqlite3 gem与sqlite数据库进行通信。 my_table数据库结构

create table my_table (
 id INTEGER PRIMARY KEY,
 posted INTEGER,
 title VARCHAR(30),
 body VARCHAR(32000));

并且在.rb文件中插入my_table的代码是:

DB.execute <<-SQL
    INSERT INTO #{table} (#{keys.join ","})
    VALUES (#{vals.join ","});
    SQL
# vals=>["1", "It happend!", "It did!"]

但它转向的sql语句将是:

 "INSERT INTO my_table (posted,title,body)\n     VALUES (1,It happend!,It did!);\n"

由于 “它发生了!” “它确实!”

我检查文档发现Array#join返回通过将数组的每个元素转换为字符串而创建的字符串。因此,数组中的双引号元素将转换为字符串并丢失双引号。并导致sql语法错误。

如何解决这个问题?

任何帮助将不胜感激! 谢谢!

2 个答案:

答案 0 :(得分:3)

这样的事情,你不应该做自己。不幸的是,mysql2 gem(我相信你正在使用)不支持预备语句,这就是你应该这样做的方式;但是您可以使用其他一些宝石来添加功能。

一个是mysql-cs-bind gem,这很简单,只需将其添加到mysql2

client.xquery(<<-SQL, vals)
    INSERT INTO #{table} (#{keys.join ","})
    VALUES (#{keys.map { "?" }.join(",")});
    SQL

另一个是使用像sequel这样更通用的宝石,它可以在各种数据库中提供很多功能,而不仅仅是MySQL。

你不应该自己做的原因是因为

  • 这是一个已解决的问题
  • 很容易出错
  • Bobby Tables可能会访问您的网站。

如果你必须亲自去做:

db.execute <<-SQL
    INSERT INTO #{table} (#{keys.join ","})
    VALUES (#{
      vals.map { |val|
        case val
        when String
          "'#{mysql.escape(val)}'"
        when Date, Time, DateTime
          "'#{val}'"
        else
          val
        end
      }.join(', ')
    });
    SQL

(不确定MySQL想要什么格式的日期/时间值,因此可能需要进行调整)

编辑:幸运的是SQLite3确实提供了预处理语句和占位符。

DB.execute <<-SQL, *vals
    INSERT INTO #{table} (#{keys.join ","})
    VALUES (#{keys.map { "?" }.join(",")});
    SQL

编辑:对map感到愚蠢。谢谢,乔丹。

答案 1 :(得分:3)

在构造SQL查询时,应避免使用字符串插值(#{...})。除了您正在经历的问题之外,这是打开SQL注入攻击的好方法。

相反,您应该使用参数绑定。这样,数据库本身就可以正确,安全地清理和引用您的值,这样您就不必担心了。

在你的情况下,它看起来像这样:

 
vals = [ "1", "It happened!", "It did!" ]
query = <<SQL
  INSERT INTO my_table (posted, title, body)
  VALUES (?, ?, ?);
SQL

DB.execute(query, vals)

你可以稍微缩短一点:

DB.execute <<-SQL, vals
  INSERT INTO my_table (posted, title, body)
  VALUES (?, ?, ?);
SQL

执行查询时,?占位符将替换为您提供的数组中的相应值 - 已清理和引用 - 作为第二个参数。

您可能已经注意到这与您的代码不完全相同,因为我已经对表名和列名进行了硬编码。 SQLite的一个限制是你不能对像这样的标识符进行参数绑定。怎么办呢?如果您从不受信任的来源获取列名(例如来自最终用户的Web请求),因此不能仅在查询中对其进行硬编码,那么您将不得不妥协。你可以做的最好的事情可能是白名单:

column_whitelist = %w[ posted title body ]

unless keys.all? {|key| column_whitelist.include?(key) }
  raise "Invalid column name '#{key}'!"
end

如果table_name来自不受信任的来源,你也会想做类似的事情。

一旦筛选了表名和列名,就可以在查询中安全地使用它们:

column_names = keys.join(", ")
placeholders = keys.map { "?" }.join(", ")

DB.execute <<-SQL, vals
  INSERT INTO #{table_name} (#{column_names})
  VALUES (#{placeholders});
SQL

P.S。如果您的表或列名称包含空格,引号或任何其他特殊字符,则需要转义并引用它们。这意味着通过在前面添加另一个双引号("变为"")然后用双引号包围整个事物来转义任何双引号。像这样的小辅助方法就可以做到:

def escape_and_quote_identifier(str)
  sprintf('"%s"', str.gsub(/"/, '""'))
end

然后你想将它应用于你的表名和列名:

table_name = escape_and_quote_identifier(table_name)
column_names = keys.map {|key| escape_and_quote_identifier(key) }
                 .join(", ")
placeholders = keys.map { "?" }.join(", ")

DB.execute <<-SQL, vals
  INSERT INTO #{table_name} (#{column_names})
  VALUES (#{placeholders});
SQL