Rails ActiveRecord/SQL 小技集


ActiveRecord関連で初心者がつまづきやすいちょっとしたこと、やり方を忘れて何度も調べてしまうものをまとめておきます。随時更新する予定。

# SQL直書きを極力避け、Railsの文法で書く
ActiveRecordはwhere内でSQLの書き方でも書けてしまうため、そちらで書いてしまったりしますが、極力rails流の文法で書くほうがきれいで、アップグレードにも対応しやすいです。

### null
```ruby:
User.where(name: nil)
# = User.where("name IS NULL")
```
参考：[[Rails]Where句にNOT NULLを指定する](https://qiita.com/kazasiki/items/8b5be52da6162b34b2b7)

### 範囲指定

`..`だと`BETWEEN :a AND :b`
`...`だと`something < :a AND something > :b`

```ruby:
User.where(created_at: 7.days.ago..3.days.ago)
# = User.where("created_at BETWEEN :a AND :b", a: 7.days.ago, b: 3.days.ago)

User.where(created_at: 7.days.ago...3.days.ago)
# = User.where("created_at > :a AND created_at < :b", a: 7.days.ago, b: 3.days.ago)
```
参考： [ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法](https://techracho.bpsinc.jp/hachi8833/2016_08_19/24876)

### IN句
Arrayで渡します。

```ruby:
User.where(name: ['John','Smith','Richard'])
# = User.where("name IN ('John','Smith','Richard')")
```

# PostgreSQL Array型を使う
PostgreSQLだと、Array型を使うことができます。

### Arrayがどれかを含む
Array中に指定した要素のいずれかあるレコードを取得するにはANYを使います。

```ruby:
# Schedule.day_of_week = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
# 'Sun'を含むレコードを抽出
Schedule.where(":day = ANY(day_of_week)", :day => 'Sun')
```

### Arrayが空のレコードを抽出

```ruby:
Schedule.where("day_of_week = {}"}
```

# 子モデル／孫モデルの結合と絞込をうまいことやる
子モデルや孫モデルをキャッシュしたり（N+1対策）、子モデル・孫モデルで条件検索しようとした場合、joins/includes/eager_load/preloadを使いますが、
慣れないうちはどんな場合にどれを使えばいいかが難しいです。
その辺のことは他にすばらしいまとめがあるので、基本的な使い分けなどはこのあたりを参考に。

- [ActiveRecordのincludes、joins、eager_load等の違いを調べてみた](https://qiita.com/viptakechan/items/94681087ec89611e3370#_reference-5a7db654c38d570420c6)
- [似ているようで全然違う!?Activerecordにおけるincludesとjoinsの振る舞いまとめ](https://qiita.com/south37/items/b2c81932756d2cd84d7d)
- [ActiveRecord ～ 複数テーブルにまたがる検索（先読み、JOINなど）](https://qiita.com/leon-joel/items/f26556c9e56833983856#_reference-116122c0d97b30b9c31a)


### 孫モデルをincludes
```ruby:
User.includes(friends: :address)
User.includes(:address, friends: [:address, :followers]) # 複数だとこうなる
```

### 子モデルをキャッシュ＆絞込
子モデルで条件検索しつつ、子モデルのデータも使いたい時にはincludesとともにreferencesも指定する必要があるようです。

```ruby:
User.includes(:friends).references(:friends).where("friends.name = ?",'John')
```

*参考* [ActiveRecord::QueryMethods#includes](http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-includes)

### 子モデル/孫モデルが存在するレコードを抽出

```ruby:
# 子モデルの場合
User.joins(:friends).where("friends.id IS NOT NULL")
# 孫モデルの場合
User.friends.joins(:followers).where("followers.id IS NOT NULL")
```

### joinsで複数の同レコードを返さないようにする
子レコードで絞込をしたい＆キャッシュはしない時にはjoinsを使いますが、親レコードが複数の子レコードを持つ場合、子レコードの数だけ結果が返ってきてしまいます。

```ruby:例
# Spot has_many :stations
Spot.all.size
# => 1
Spot.first.stations.size
# => 3

spots = Spot.joins(:stations).where("stations.name = hoge")
spots.size
# => 3
```
というような感じで、上の例だと同じレコードが3件返ってきてしまいます。
`Spot.all.size`が1なので、ここで戻ってきてほしいのは1件だけです。レコードが重複をせずuniqになるようにするにはdistinctを使います。

```ruby:
spots = Spot.joins(:stations).where("stations.name = hoge").distinct
spots.size
# => 1
```

### distinctとorderを両方使いたい
*参考* [Rails tips: Active Recordの`#from`を使ってorderとdistinctを1つのクエリにする（翻訳）](https://techracho.bpsinc.jp/hachi8833/2018_02_09/52301)

# ORを使いこなす
普通の書き方は特に難しいことはありません。

```ruby:
Spot.where(prefecture: 'kanagawa').or(Spot.where(city: 'machida'))
```

### 子モデルでOR

```ruby:
john = User.find_by(name: 'John')
john.friends.where(gender: 'male').or(john.friends.where(name: 'Maria'))

# これはだめ
john.friends.where(gender: 'male').or(Friend.where(name: 'Maria'))

```


### 結合先（子モデル）の条件でORを使う

```ruby:
User.joins(:friends).where("friends.gender = male").or(User.joins(:friends).where("friends.name = Maria"))

# これもやっぱりだめ
User.joins(:friends).where("friends.gender = male").or(where("friends.name = Maria"))

```

ORの前と後ろはきちんと同じ形式になっていないとダメみたいです。

参考： [ActiveRecord で join と or と and が入り混じった場合](https://pinzolo.github.io/2017/03/17/activerecord-join-or-and.html)