はじめに
Rails7.1.2では、SQLへ変数を埋め込む際に不具合があることを学びました。
この記事ではその内容をまとめます。
バージョンはRails7.1.2, MySQL8.0です。
※この記事は2024年10月7日時点の情報に基づいています。
【Rails7.1.2】SQLにバインド変数を使うと数値が文字列として展開されてしまう
バインド変数の確認のために以下のサンプルコードを用います。
class Article < ApplicationRecord
def self.fetch_with_limit(limit)
sql = <<-SQL
SELECT * FROM articles LIMIT ?
SQL
Article.find_by_sql([sql, limit])
end
end
DBにはArticleのレコードが10件登録済みです。
irb(main):004> Article.count
Article Count (1.5ms) SELECT COUNT(*) FROM `articles`
=> 10
この状況で先ほどのサンプルコードのdef self.fetch_with_limit(limit)
を実行すると、次のエラーが出てしまいます。
irb(main):005> Article.fetch_with_limit(5)
Article Load (5.1ms) SELECT * FROM articles LIMIT '5'
/usr/local/bundle/gems/mysql2-0.5.5/lib/mysql2/client.rb:151:in `_query': Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''5'' at line 1 (ActiveRecord::StatementInvalid)
/usr/local/bundle/gems/mysql2-0.5.5/lib/mysql2/client.rb:151:in `_query': You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''5'' at line 1 (Mysql2::Error)
詳しく見てみると、LIMIT 5
となってほしいのに、LIMIT '5'
となっています。
つまり、埋め込んだ値が文字列として展開されてしまっているのです。
Rails7.0以降に存在する既知の不具合のようで、Issueがいくつかありました。
なお、この事象はSQLite3では発生しませんでした(PostgreSQLでは未検証です)
対処法
もう一度、先ほどのサンプルコードを確認します。
class Article < ApplicationRecord
def self.fetch_with_limit(limit)
sql = <<-SQL
SELECT * FROM articles LIMIT ?
SQL
Article.find_by_sql([sql, limit])
end
end
対処法として、単純に文字列展開して値を埋め込むことができます。
class Article < ApplicationRecord
def self.fetch_with_limit(limit)
sql = <<-SQL
SELECT * FROM articles LIMIT #{limit}
SQL
Article.find_by_sql(sql)
end
end
ただしこれだけだとSQLインジェクションを引き起こす可能性があります。
次のようにガード節を追加するなど、Integer以外の値が決して埋め込まれないように配慮しなくてはなりません。
def self.fetch_with_limit(limit)
# SQLインジェクションを防ぐ
unless limit.is_a?(::Integer)
raise ::ArgumentError, 'limit must be an Integer.'
end
sql = <<-SQL
SELECT * FROM articles LIMIT #{limit}
SQL
Article.find_by_sql(sql)
end
これで再度実行してみます。
irb(main):002> Article.fetch_with_limit(5)
Article Load (8.3ms) SELECT * FROM articles LIMIT 5
=>
[#<Article:0x0000ffff9e66c1c0
id: 1,
title: "タイトル1",
description: "記事の詳細1",
created_at: Thu, 17 Oct 2024 11:13:28.322262000 UTC +00:00,
updated_at: Thu, 17 Oct 2024 11:13:28.322262000 UTC +00:00>,
#<Article:0x0000ffff9e66c080
id: 2,
title: "タイトル2",
description: "記事の詳細2",
created_at: Thu, 17 Oct 2024 11:13:28.330934000 UTC +00:00,
updated_at: Thu, 17 Oct 2024 11:13:28.330934000 UTC +00:00>,
#<Article:0x0000ffff9e66bf40
id: 3,
title: "タイトル3",
description: "記事の詳細3",
created_at: Thu, 17 Oct 2024 11:13:28.337640000 UTC +00:00,
updated_at: Thu, 17 Oct 2024 11:13:28.337640000 UTC +00:00>,
#<Article:0x0000ffff9e66be00
id: 4,
title: "タイトル4",
description: "記事の詳細4",
created_at: Thu, 17 Oct 2024 11:13:28.343972000 UTC +00:00,
updated_at: Thu, 17 Oct 2024 11:13:28.343972000 UTC +00:00>,
#<Article:0x0000ffff9e66bcc0
id: 5,
title: "タイトル5",
description: "記事の詳細5",
created_at: Thu, 17 Oct 2024 11:13:28.366889000 UTC +00:00,
updated_at: Thu, 17 Oct 2024 11:13:28.366889000 UTC +00:00>]
きちんとLIMIT 5
で展開されたSQLが実行されています。
おわりに
Railsのバージョンアップに関わることでさまざまな発見があります。
引き続きアウトプットを続けます。