SQL の記述に ERB を使う

# はじめに

Rails などで Active Record を使っていても生の SQL を記述したいケースはままあります。その際に SQL ファイル内に変数や条件分岐を埋め込めると便利です。そこで、今回 ERB テンプレートを使って実現してみました。

# 例

特定の条件下のみ WHERE 句を使うというケースを想定しています。

## 方法: ERB#result_with_hash を使う (2024/11/07 (木) 更新)

```sample1.sql.erb
SELECT
  characters.name,
  animes.title
FROM
  characters
  INNER JOIN anime_characters
    ON anime_characters.character_id = characters.id
  INNER JOIN animes
    ON animes.id = anime_characters.anime_id
<% if title %>
WHERE
  animes.title = '<%= title %>'
<% end %>
;
```
```rb
require 'erb'

template = ERB.new(File.read('sample1.sql.erb'), trim_mode: '<>')
puts(template1.result_with_hash(title: nil))
```
```sql
SELECT
  characters.name,
  animes.title
FROM
  characters
  INNER JOIN anime_characters
    ON anime_characters.character_id = characters.id
  INNER JOIN animes
    ON animes.id = anime_characters.anime_id
;
```
```rb
puts(template1.result_with_hash(title: '進撃の巨人'))
```
```sql
SELECT
  characters.name,
  animes.title
FROM
  characters
  INNER JOIN anime_characters
    ON anime_characters.character_id = characters.id
  INNER JOIN animes
    ON animes.id = anime_characters.anime_id
WHERE
  animes.title = '進撃の巨人'
;
```

## 過去に書いていた方法

### 方法 1: ERB を使う

```sample1.sql.erb
SELECT
  characters.name,
  animes.title
FROM
  characters
  INNER JOIN anime_characters
    ON anime_characters.character_id = characters.id
  INNER JOIN animes
    ON animes.id = anime_characters.anime_id
<% if defined?(:title) %>
WHERE
  animes.title = '<%= title %>'
<% end %>
;
```
```rb
require 'erb'

# 第 3 引数を指定しないと <%= %> の行が空行として出力されてしまう。
template1 = ERB.new(File.read('sample1.sql.erb'), nil, '<>')

puts(template1.result(binding))
#=> NameError: undefined local variable or method `title' for main:Object

title = '偽物語'
puts(template1.result(binding)) # 出力結果は stdout2

title = nil
puts(template1.result(binding)) # 出力結果は stdout1
```

```:stdout1
SELECT
  characters.name,
  animes.title
FROM
  characters
  INNER JOIN anime_characters
    ON anime_characters.character_id = characters.id
  INNER JOIN animes
    ON animes.id = anime_characters.anime_id
;
```

```:stdout2
SELECT
  characters.name,
  animes.title
FROM
  characters
  INNER JOIN anime_characters
    ON anime_characters.character_id = characters.id
  INNER JOIN animes
    ON animes.id = anime_characters.anime_id
WHERE
  animes.title = '偽物語'
;
```

[ERB#result](https://docs.ruby-lang.org/ja/latest/method/ERB/i/result.html) には Binding オブジェクトを渡す必要があります。通常 [Kernel.#binding](https://docs.ruby-lang.org/ja/latest/method/Kernel/m/binding.html) を渡しますが、できれば ERB テンプレート描画時に必要な値のみを渡したいです。

### 方法 2: [Tilt](https://github.com/rtomayko/tilt) を使う

```
$ gem install tilt
```

```sample2.sql.erb
SELECT
  characters.name,
  animes.title
FROM
  characters
  INNER JOIN anime_characters
    ON anime_characters.character_id = characters.id
  INNER JOIN animes
    ON animes.id = anime_characters.anime_id
<% if locals.key?(:title) %>
WHERE
  animes.title = '<%= title %>'
<% end %>
;
```

```rb
require 'erb'
require 'tilt'

template2 = Tilt.new('sample2.sql.erb')

puts(template2.render(nil)) # 出力結果は stdout1 と同じ
puts(template2.render(nil, title: '偽物語')) # 出力結果は stdout2 と同じ
```

[Tilt#render](http://www.rubydoc.info/github/rtomayko/tilt/Tilt%2FTemplate%3Arender) の第 2 引数に渡した hash は、ERB テンプレート中では `locals` という変数で参照できます。

# 参考

* [Ruby 2.4.0 リファレンスマニュアル » ライブラリ一覧 » erb ライブラリ » ERB クラス](https://docs.ruby-lang.org/ja/latest/class/ERB.html)
* [rtomayko/tilt](https://github.com/rtomayko/tilt)