1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails7.1.2】SQLにバインド変数を使うと数値が文字列として展開されてしまう

Posted at

はじめに

Rails7.1.2では、SQLへ変数を埋め込む際に不具合があることを学びました。

この記事ではその内容をまとめます。

バージョンはRails7.1.2, MySQL8.0です。

※この記事は2024年10月7日時点の情報に基づいています。

【Rails7.1.2】SQLにバインド変数を使うと数値が文字列として展開されてしまう

バインド変数の確認のために以下のサンプルコードを用います。

app/models/article.rb
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では未検証です)

対処法

もう一度、先ほどのサンプルコードを確認します。

app/models/article.rb
class Article < ApplicationRecord
  def self.fetch_with_limit(limit)
    sql = <<-SQL
      SELECT * FROM articles LIMIT ?
    SQL

    Article.find_by_sql([sql, limit])
  end
end

対処法として、単純に文字列展開して値を埋め込むことができます。

app/models/article.rb
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以外の値が決して埋め込まれないように配慮しなくてはなりません。

app/models/article.rb
  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のバージョンアップに関わることでさまざまな発見があります。

引き続きアウトプットを続けます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?