ActiveRecord・ArelでSQLを操作する


## 環境
- バージョン
  - `Rails 4.2`
  - `Ruby 2.4.7`



# Arelとは
:::note
- Ruby on Railsのクエリビルダーであり、Rubyオブジェクトを使用してSQLクエリを構築するためのライブラリ
- Rubyのオブジェクト指向プログラミングの概念を取り入れ、クエリの構造をオブジェクトとして表現することが出来る
- ActiveRecordと密接に統合されており、ActiveRecordのクエリインターフェイスと同様に使用することができる。これにより、ActiveRecordを使用してデータベースとのやり取りを簡単に行うことが可能
:::

:::note warn
- Arelは、Railsの公式ドキュメントで推奨されていない
    - 将来的に削除される可能性があるため、保守性の問題がある
- RailsのバージョンによってはArelのバージョンと互換性がない可能性がある
- よって、Railsの公式ドキュメントではActiveRecordのクエリビルダーを使用することが推奨されている
:::


# ActiveRecordとは（一部抜粋）
- ActiveRecordとは、MVCで言うところのモデルに相当する
- ORM（Object-Relational Mapping）システムに記述されている「ActiveRecordパターン」を実装したもの
  - ORMとは
      - プログラムとデータベースの間の橋渡しを行うためのフレームワーク
      - 本来、プログラム内のオブジェクトとDB内のテーブルとの間には、プロパティの名称・データ型などの相違があり、プログラム内でデータベースにアクセスする場合には、手動でデータの変換やマッピングを行う必要がある
      - ORMを使用することで、プログラム内のオブジェクトとデータベースのテーブルを自動的にマッピングすることが可能
      - これにより、プログラム内でオブジェクトを直接操作することが出来る
>SQL文を直接書く代わりにわずかなアクセスコードを書くだけで、アプリケーションにおけるオブジェクトの属性やリレーションシップをデータベースに保存することも、データベースから読み出すこともできるようになります。

- ActiveRecordモデルを使用してリレーショナルデータベースに保存されたデータを操作する

https://railsguides.jp/active_record_basics.html


# 使用例

※アソシエーションは以下の通り
```rb:member.rb
class Member < ActiveRecord::Base
  has_many :posts
end
```
```rb:post.rb
class Post < ActiveRecord::Base
  belongs_to :member
end
```

## 内部結合


### ActiveRecordのみ

```rb
members = Member.joins(:post)
                .where(posts: { type: 1, genre: 2 })
# puts members.to_sql
```

---

### 条件のみArel
```rb
# ActiveRecord_Relation
query = Member.joins(:post)
              .where(post_table[:type].eq(1))
              .where(post_table[:genre].eq(2))
```

---

### Arelのみ

```rb
member_table = Member.arel_table
post_table = Post.arel_table

# Arel::SelectManager
query = member_table.join(post_table).on(post_table[:id].eq(member_table[:post_id]))
                    .where(post_table[:type].eq(1))
                    .where(post_table[:genre].eq(2))

# => SELECT FROM members INNER JOIN posts ON posts.id = members.post_id WHERE posts.type = 1 AND posts.genre = 2
```




## 外部結合

### ActiveRecordのみ
```rb
Member.joins('LEFT OUTER JOIN posts ON posts.id = members.post_id')
# => SELECT members.* FROM members LEFT OUTER JOIN posts ON posts.id = members.post_id
```
※Rails 5.0以降では`left_joins`（エイリアスは`left_outer_joins`）メソッドを使用可能

---

### 条件のみArel
```rb
member_table = Member.arel_table
post_table = Post.arel_table

join_condition = member_table.outer_join(post_table).on(post_table[:member_id].eq(member_table[:id])).join_sources

# ActiveRecord_Relation
members = Member.joins(join_condition)
                .where(post_table[:type].eq(1))
                .where(post_table[:genre].eq(2))

# => SELECT members.* FROM members LEFT OUTER JOIN posts ON posts.member_id = members.id WHERE posts.type = 1 AND posts.genre = 2
```


---

### Arelのみ
```rb
member_table = Member.arel_table
post_table = Post.arel_table

# Arel::SelectManager
members =
   member_table
     .project(member_table[Arel.star])
     .outer_join(post_table).on(member_table[:id].eq(post_table[:member_id]))

# => SELECT members.* FROM members LEFT OUTER JOIN posts ON members.id = posts.member_id
```

もしくは
```rb
member_table.join(post_table, Arel::Nodes::OuterJoin).on(member_table[:id].eq(post_table[:member_id]))
```

---

### Arelで外部結合をした後に、ActiveRecordを返す
```rb
# Arelのテーブルオブジェクトを作成する
member_table = Member.arel_table
post_table = Post.arel_table

# Arelで外部結合を作成する
join = member_table.outer_join(post_table).on(member_table[:id].eq(post_table[:member_id])) # Arel::SelectManager

# ActiveRecordでSQLを実行する
Member.joins(join.to_sql) # ActiveRecord_Relation
```


## EXISTS句
```rb
member_table = Member.arel_table
post_table = Post.arel_table

condition = Member
              .where(member_table[:id].eq(post_table[:member_id]))
              .where(member_table[:id].eq(1))
              .exists

@posts = Post.where(condition)
# => SELECT posts.* FROM posts WHERE (EXISTS (SELECT members.* FROM members WHERE members.id = posts.member_id AND members.id = 1))
```

## WITH句
自分でテーブルを作り、そいつをいつもと同じ方法で扱うイメージ
```rb
# 会員テーブル（DBに存在する）
member_table = Member.arel_table
# WITH句で自作するテーブル（DBに存在しない）
original_table = Arel::Table.new(:originals)

with_query = member_table.project(member_table[:birthday].maximum.as('youngest_birthday'))

query = member_table
          .project(member_table[Arel.star])
          .from([member_table, original_table])
          .where(member_table[:birthday].eq(original_table[:youngest_birthday]))
          .with(Arel::Nodes::As.new(original_table, with_query))

# => WITH originals AS (SELECT MAX(members.birthday) AS youngest_birthday FROM members) SELECT members.* FROM members, originals WHERE members.birthday = originals.youngest_birthday
```


# 実装時に気をつけること

## N+1問題を回避するために、以下のメソッドでの実装も検討する

:::note warn
N+1問題とは、データベースへのアクセス回数が余計に多くなってしまう現象のこと
:::

### eager_loadメソッド
- 外部結合を使用し、1度のクエリで関連するモデルのデータを取得しキャッシュする
- 結合しているので`preload`とは違い、associationの値での絞り込みが可能
```rb
# 指定したテーブルをLEFT OUTER JOINで引いてキャッシュする
Member.eager_load(:posts).where(posts: { id: 1 })
```

### preloadメソッド
- 指定したassociationを複数のクエリに分けて引いてキャッシュする
- データ量が増えるほど、`eager_load`よりも`preload`の方がSQLを分割して取得するため、レスポンスタイムは早くなる
- 結合をしていない為、assosiationの値で絞り込むことは出来ない
```rb
Post.all.preload(:member)
```

### includesメソッド
- includesしたテーブルでwhereによる絞り込みを行う
- includesしたassociationに対して、joinsまたはreferencesを呼んでいる
- 条件（where）などでアソシエーション先のデータを
    - 参照している場合 : `eager_load`
    - 参照していない場合 : `preload`
        - よしなに対応してくれるが、意図しないパフォーマンスにならないかどうか注意が必要

```rb
# preloadの挙動
Member.includes(:posts)
```
```rb
# eager_loadの挙動
Member.includes(:posts).where(posts: { id: 1 })
```


## デバッグ

### ActiveRecordのログを標準出力に書き出す

```rb
ActiveRecord::Base.logger = Logger.new(STDOUT)
```
ActiveRecord::Baseは、Railsのデータベース接続を行うためのモデルクラス。
このクラスを継承することで、データベースのテーブルに対応するモデルを作成することができる。

loggerは、Railsのログ出力を管理するためのオブジェクト。
**Logger.new(STDOUT)** は、ログを標準出力に出力するように設定している。

この設定により、データベースに対するクエリやトランザクションの実行結果などがログに出力されるようになる。
開発やデバッグの際に、実行されたSQLクエリを確認することができる。

### 例
N+1問題が発生する場合、**同じSQLクエリが複数回実行される**ことが分かる。

```rb
ActiveRecord::Base.logger = Logger.new(STDOUT)

# 投稿日降順、10件
posts = Post.order(posted_date: :desc).limit(10)
# 取得してきたPostに紐づくMemberが持つpoint_codeを表示する処理
posts.each { |post| p post.member.point_code }
```

```lua
D, [2023-03-16T18:46:46.678392 #73414] DEBUG -- :   Post Load (0.5ms)  SELECT  "posts".* FROM "posts"  ORDER BY "posts"."posted_date" DESC LIMIT 10
D, [2023-03-16T18:46:46.679827 #73414] DEBUG -- :   Member Load (0.2ms)  SELECT  "members".* FROM "members" WHERE "members"."id" = $1 LIMIT 1  [["id", 8]]
"0000008"
D, [2023-03-16T18:46:46.680974 #73414] DEBUG -- :   Member Load (0.1ms)  SELECT  "members".* FROM "members" WHERE "members"."id" = $1 LIMIT 1  [["id", 1]]
"0000001"
D, [2023-03-16T18:46:46.682042 #73414] DEBUG -- :   Member Load (0.2ms)  SELECT  "members".* FROM "members" WHERE "members"."id" = $1 LIMIT 1  [["id", 2]]
"0000002"
D, [2023-03-16T18:46:46.682901 #73414] DEBUG -- :   Member Load (0.2ms)  SELECT  "members".* FROM "members" WHERE "members"."id" = $1 LIMIT 1  [["id", 3]]
"0000003"
D, [2023-03-16T18:46:46.683772 #73414] DEBUG -- :   Member Load (0.2ms)  SELECT  "members".* FROM "members" WHERE "members"."id" = $1 LIMIT 1  [["id", 4]]
"0000004"
```
この場合、Postを取得するために、Memberのクエリが5回発行されている。これが**N+1問題**。

:::note
**N+1問題を解決するためには・・・**
`preload`, `eager_load`, `includes`といったメソッドを使用し、関連するモデルをまとめて取得するなどの方法がある。
:::

例えば、次のように書くことで、Memberのレコードを1度だけで取得することが可能。

#### `eager_load`の場合

```rb
ActiveRecord::Base.logger = Logger.new(STDOUT)

# 外部結合で一括取得しキャッシュ
posts = Post.eager_load(:member).order(posted_date: :desc).limit(10)
posts.each { |post| p post.member.point_code }
```

```lua
D, [2023-03-16T20:17:57.470509 #81978] DEBUG -- :   SQL (1.5ms)  SELECT  "posts"."id" AS t0_r0, "posts"."member_id" AS t0_r1, "posts"."posted_date" AS t0_r2, "posts"."body" AS t0_r3, "posts"."genre" AS t0_r4, "posts"."post_status", "posts"."created_system" AS t0_r5, "posts"."updated_system" AS t0_r6, "posts"."created_user" AS t0_r7, "posts"."updated_user" AS t0_r8, "posts"."created_at" AS t0_r9, "posts"."updated_at" AS t0_r10, "members"."id" AS t1_r0, "members"."mail_address" AS t1_r1, "members"."mail_address_for_index" AS t1_r2, "members"."member_code" AS t1_r3, "members"."point_code" AS t1_r4, "members"."first_name" AS t1_r5, "members"."first_name_kana" AS t1_r6, "members"."last_name" AS t1_r7, "members"."last_name_kana" AS t1_r8, "members"."sex_division" AS t1_r9, "members"."birthday" AS t1_r10, "members"."created_system" AS t1_r11, "members"."updated_system" AS t1_r12, "members"."created_user" AS t1_r13, "members"."updated_user" AS t1_r14, "members"."created_at" AS t1_r15, "members"."updated_at" AS t1_r16, "members"."test_account_flag" AS t1_r17 FROM "posts" LEFT OUTER JOIN "members" ON "members"."id" = "posts"."member_id"  ORDER BY "posts"."posted_date" DESC LIMIT 10
"0000008"
"0000001"
"0000002"
"0000003"
"0000004"
```



#### `includes`の場合

```rb
ActiveRecord::Base.logger = Logger.new(STDOUT)

# includesしたテーブルは参照していないので、preloadの挙動になる
posts = Post.includes(:member).order(posted_date: :desc).limit(10)
posts.each { |post| p post.member.point_code }
```

```lua
D, [2023-03-16T18:51:09.793843 #73945] DEBUG -- :   Post Load (0.5ms)  SELECT  "posts".* FROM "posts"  ORDER BY "posts"."posted_date" DESC LIMIT 10
D, [2023-03-16T18:51:09.799642 #73945] DEBUG -- :   Member Load (4.1ms)  SELECT "members".* FROM "members" WHERE "members"."id" IN (8, 1, 2, 3, 4)
"0000008"
"0000001"
"0000002"
"0000003"
"0000004"
```



### その他Railsアプリケーションのデバッグについては以下を参照
https://railsguides.jp/debugging_rails_applications.html




# その他参考
- [Arelの使い方（条件式）](https://kinosuke.hatenablog.jp/entry/2016/03/30/153236)
- [【Rails】N+1を回避するメソッド(includes, eagar_load, preload)の使い分けについて](https://zenn.dev/d0ne1s/articles/fdf4832d478da6)
- [【Rails】N+1問題をアラート表示してくれるgem「bullet」を初心者向けにまとめてみた](https://techtechmedia.com/nplusone_query-bullet-rails/)
- [ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い](https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58)
- [ActiveRecordのincludesは使わずにpreloadとeager_loadを使い分ける理由](https://qiita.com/ryosuketter/items/097556841ec8e1b2940f)

https://atl2.net/webtool/sql%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%82%BF%E3%83%BCfor-web/