Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails 8.1: 生SQLやArelのブーリアン型カラムをTRUEやFALSEと比較できるようになった(翻訳)

概要

CC BY-NC-SA 4.0 International Deedに基づいて翻訳・公開いたします。

CC BY-NC-SA 4.0 Deed | 表示 - 非営利 - 継承 4.0 国際 | Creative Commons

日本語タイトルは内容に即したものにしました。

Rails 8.1: 生SQLやArelのブーリアン型カラムをTRUEやFALSEと比較できるようになった(翻訳)

私がRailsアプリケーションで何かする場合、ほぼすべてのクエリをActive Recordで書いています。

class Post
  scope :published, -> { where(published: true) }
end

Post.published.to_sql
# => SELECT "posts".* FROM "posts" WHERE "posts"."published" = TRUE

しかしクエリが複雑になりすぎる場合は生SQLを手書きする必要が生じることもあります。生SQLを使うようになると、ORM(Object-Relational Mapping)を使うメリットのひとつである、アプリケーションのメンテナンスしやすさを維持するのが難しくなることがあります。

仮に上のコードが標準のActive Recordでは表現できず、何らかの生SQLを使う必要があるとします。MySQLの場合は以下のようになります。

Post.where("published = 1").to_sql
# => SELECT `posts`.* FROM `posts` WHERE published = 1

この例は、最初の例と2つの点で異なっています。
1つ目は、MySQLでは識別子をバッククォート(``)で囲んでいること。
2つ目は(こちらの方が重要なのですが)、MySQLではpublishedTRUEではなく1と比較しています。

もしこの生SQLをPostgreSQLで実行したら、以下のエラーが発生します。

PG::UndefinedFunction: ERROR:  operator does not exist: boolean = integer (ActiveRecord::StatementInvalid)
LINE 1: SELECT "posts".* FROM "posts" WHERE (published = 1) /*applic...

🔗 BOOLEAN型の意味はRDBMSによって異なる可能性がある

MySQLでpublished = 1という比較が動作する理由は、MySQLにはBOOLEANというカラム型が実際には存在しないためです。一応MySQLにはBOOLBOOLEANというエイリアスは存在しますが、内部ではTINYINT(1)というカラム型を参照しています。

この点はSQLiteも同じなのですが、SQLiteではBOOLEANINTEGER型に対応付けられている点が異なります。

MySQLとSQLiteはどちらも内部でBOOLEANに整数型を利用しているため、published = 1のようなブーリアン型カラムへのクエリが可能です。しかし残念なことに、PostgreSQLのBOOLEAN型は本物の型です。つまり、PostgreSQLのBOOLEAN型を比較するときは必ずTRUEFALSEと比較しなければなりません。

ありがたいことに、TRUEFALSEは実はMySQLやSQLiteでも使えます。BOOLEANがMySQLやSQLiteのinteger型のエイリアスになっているのと同様に、TRUE1のエイリアスであり、FALSE0のエイリアスになっています。つまり、PostgreSQL/MySQL/SQLiteという3つのデータベースすべてで機能するクエリを作成する場合は、BOOLEAN型のカラムを比較するときに常にTRUEFALSEを使う必要があります。

原注

SQLite 3.23.0まではTRUEFALSEのエイリアスが追加されていなかったため、Active Record 8.0までは、古いバージョンとの互換性を保つためにSQLite用のクエリで10を使っていました。ただし、Active Record 8.1以降ではSQLiteの最小サポートバージョンが3.23.0になるため(809abd3)、Active RecordはArel(34bebf3)と標準のActive Record(576db3b)の両方のクエリでTRUEFALSEを利用可能になります。

10 ではなくTRUEFALSEを使うことには別のメリットもあります。
10を使っているクエリを読む場合は、そのカラム型がブール型か整数型かを適切に判断できるでしょうか?publishedなどのようにブール型らしさが伝わるカラム名ならまだしも、現実にはもっとわかりにくいカラム名である可能性もあります。カラムの比較にTRUEFALSEを使っておけば、今後そのカラムがブール型であることをすぐに理解できるようになります。

🔗 互換性を失わないようにSQLクエリを書く

そういうわけで、生SQLでBOOLEAN型のカラムを使う必要が生じたら、以下のようにブーリアンリテラル(TRUEFALSE)を使うようにしましょう!

Post.where("published = TRUE").to_sql
# => SELECT "posts".* FROM "posts" WHERE published = TRUE

こうすることで、PostgreSQL/MySQL/SQLiteのどれでもSQLクエリの互換性が維持されるようになり、クエリのカラム型も明確になるので今後クエリが読みやすくなります。

関連記事

Rails: Solid Queueで重要なUPDATE SKIP LOCKEDを理解する(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。