ActiveRecord入門

# 概要

Railsの復習に伴って、ActiveRecordの最低限の知識を整理しました。

# ActiveRecordとは

- Ruby on Rails
	* ORマッパーにActiveRecordを採用している。
	* **ActiveRecordはRuby on Railsが開発している。**
* OR Mapper (Object <> RDB)
	* SQLを意識せずにデータベースを扱う事が可能。
	* DBエンジンは原則なんでもOK。
* Railsを利用せずとも、単体でも利用可能

# 命名規則

- 非常に厳格
* テーブル名は「小文字 + 複数形」
* created_atとupdated_atで作成日時の更新日時を自動保存できる。

# ActiveRecordの利用の流れ
- ① DBにテーブルを作成
* ② Ruby上でactive_recordをrequire
* ③ Timezoneを日本に設定
* ④ ActiveRecord::BaseにDB接続情報を記載
* ⑤ テーブル名に基づくクラスを作成
* ⑥ 上記のクラスからインスタンスを作成
	* インスタンスが1レコードになる

# ActiveRecordの基本文法
## INSERT(CREATE)
- インスタンス化
	* レコードを作成準備
* <instance>.save
	* レコードの保存
* <instance>.create
	* レコードの作成と保存を同時に行う

```insert.rb
require 'active_record'
require 'pp'
require 'logger'

# Timezoneの設定
Time.zone_default = Time.find_zone! 'Tokyo'
ActiveRecord::Base.default_timezone = :local
# ロガーの設定
ActiveRecord::Base.logger = Logger.new(STDOUT)
# DB情報
ActiveRecord::Base.establish_connection(
  "adapter" => "sqlite3",
  "database" => "./myapp.db"
)
# Modelを定義
class User < ActiveRecord::Base
end
# Modelのインスタンスが1レコードとなる
user = User.new(
  name: 'fujimoto',
  age: 27,
)
user.save

user = User.new
user.name = 'fujimoto'
user.age = 27
user.save

User.create(
name: 'fujimoto',
age: 27,
)
```	

## SELECT(READ)
### 特徴
- 検索条件に引っかからなければnilが返される。
* エラーで返したい場合は、メソッドに!をつける。
### メソッド
- <Model>.all
	* 全件取得
* <Model>.select(‘<col>, <col>, …’).all
	* カラムを絞って取得
* <Model>.first
	* 最初のレコードを取得
	* 引数を渡すと「最初の○件」の様に取得も可能
* <Model>.last
	* 最後のレコードを取得
	*  引数を渡すと「最後の○件」の様に取得も可能

```select1.rb
# 全件取得
pp User.all
# カラムを絞って全件取得
pp User.select('id, name, age').all
# 最初の3件を取得
pp User.select('id, name, age').first(3)
# 最後の2件を取得
pp User.select('id, name, age').last(2)
```
	
* <Model>.find(<id>)
	* idで条件取得
	* データの取得は1件のみ
* <Model>.find_by(<col>: <value>)
	* カラム名で検索
	* <Model>.find_by_<col>(<value>)も可
	* データの取得は1件のみ
* <Model>.where(<col>: <value>)
	* 返り値は必ず配列
* <Model>.where(‘<条件>’)
	* SQLの条件ぽく条件を書くことも可能
* <Model>.where(‘<条件>’).where(‘<条件>’)
	* AND検索
* <Model>.where(‘<条件>’).or(<Model>.where(‘<条件>’))
	* OR検索
* <Model>.where.not(‘条件’)
	* NOT検索

```select2.rb
# idで条件を指定して検索
pp User.select('id, name, age').find(2)
# 特定のカラムで条件を指定して検索(3パターン)
pp User.select('id, name, age').find_by(name: 'maeda')
pp User.select('id, name, age').find_by name: 'horio'
pp User.select('id, name, age').find_by_name! 'urano'
# AND検索
pp User.select('id, name, age')
  .where('age >= 25')
  .where('age < 30')
# OR検索
pp User.where(age: 27)
  .or(User.where(age: 33))
  .select('id, name, age')
# NOT検索
pp User.where.not(id: 3).select('id, name, age')
```
## DELETE(DELETE)
* <Model>.destroy
	*  レコードを削除する。
	* 低速
	* Validationなどの追加機能が考慮される
	* UPDATEのupdate
- <Model>.delete
	* 単純にレコードを削除する。
	* 高速
	* Validationなどの追加機能は考慮されない
	* UPDATEのupdate_all
*  <Model>.delete_all
	* deleteの全件削除バージョン

```destroy.rb
# destroyでidを指定して削除
User
  .destroy(3)
# 条件を指定して、条件に合致するレコードを全件削除
User
  .where('name like ?', '%a%')
  .delete_all

users = User.select('id, name, age').all
pp users
```	

## UPDATE(UPDATE)
- <instance>.update(<id>, <col>)
	* idを指定してカラムを更新する
	* id以外の条件でレコードを指定する場合はwhereを利用する
	* ValidationなどActiveRecordの追加機能を利用できるが低速。
* <instance>.update_all(‘更新内容’)
	* SQLのUPDATEっぽくupdateできる。
	* ValidationなどActiveRecordの追加機能は利用できないが、高速。

```update.rb
# idを指定してレコードを更新
User.update(4, age: 26)
# 条件を指定してレコードを更新
User.where('age = 26').update(age: 27)
# SQLのUPDATEっぽくレコードを更新
User.update_all('age = 26')

users = User.select('id, name, age').all
pp users
```

# Logger
- Rubyの標準ライブラリ
* requireして利用する
* ActiveRecord::Base.loggerに出力場所を代入するとActiveRecordで発行されたSQLを確認する事ができる。

# プレースホルダー
- 変数に値を保持して、条件に当てる場合、直接変数を展開してはいけない。
	* 悪意のあるコードが代入される可能性がある為。
* 条件に変数を利用したい場合は、プレースホルダーを利用する。

```placeholder.rb
min = 25
max = 35

age = {
  min: min,
  max: max,
}
# プレースホルダーに?を利用した場合
pp User.select('id, name, age')
  .where('age >= ? and age < ?', min, max)
# プレースホルダーにシンボルを利用した場合
pp User.select('id, name, age')
  .where('age >= :min and age < :max', age)
# like検索でもプレースホルダーを利用する
pp User.select('id, name, age')
  .where('name like ?', '%f%')

```

# 並び順
- order
	* カラムを元に並び替えを行う。

```order.rb
# 単純に年齢で並び替える
users = User.select('id, name, age')
  .order(:age)
# 逆順で年齢で並び替える
users = User.select('id, name, age')
  .order(age: :desc)
# 最初の1件を飛ばして、3件若い順に年齢を取得する
users = User.select('id, name, age')
  .order(:age)
  .limit(3)
  .offset(1)

pp users
```

* limit
	- 取得数を制限して取得する。
* offset
	* 何番目から取得するかを指定して取得する。

# 条件の登録
- 条件の登録はActiveRecord::Baseクラスを継承したModel内にクラスメソッドを定義するか、scopeを定義する事で実現する。

```limit.rb
# 条件の登録
class User < ActiveRecord::Base
	# クラスメソッドVer.
  def self.top(num)
    select('id, name, age')
      .order(:age)
      .limit(num)
  end
	# scopeVer.
  scope :top, -> (num) {
    select('id, name, age')
      .order(:age)
      .limit(num)
  }
end

pp User.top(1)
```

# レコードが見つからない場合のみ挿入
- メールアドレスを検索してなければ登録的な処理で使える。
* find_or_create_by
	* 条件検索に一致しない場合のみ登録し、条件に合致する内容があれば取得する。
	* 条件に一致する内容がない場合に、他のカラムも埋めたい場合は、ブロックを利用する。

```find_or_create_by.rb
# find_or_create_by
## 条件に一致する内容がない場合は、ブロックも実行する。
user = User.select('id, name, age')
  .find_or_create_by(name: 'nu') do |u|
    u.age = 33
end

pp user
```	

# Validation
- レコード挿入前にデータのチェックを行う事ができる
* nameとageが必須で、nameは3文字以上のバリデーション

```validation.rb
# nameとageがnilのデータを挿入
user = User.new(
  name: nil,
  age: nil,
)
# データの保存が成功すればok、失敗すればng\\\
if user.save
  p 'ok'
else
  pp user.errors.messages
end
# データの保存が失敗すればエラーを出す
# user.save!

users = User.select('id, name, age').all
pp users
```

# Callback
- 特定の処理が行われる前後に自動的に処理を入れる。
* 各種CRUD系にbefore_, after_を付与する。
* モデル内に記載する。

```callback.rb
class User < ActiveRecord::Base
  before_destroy :print_before_msg
  after_destroy :print_after_msg

  protected
    def print_before_msg
      puts "#{self.name} will be deleted"
    end
    def print_after_msg
      puts "#{self.name} deleted"
    end
end


User.where('age >= 20').destroy_all

pp User.select('id, name, age').all

```

# Association
- テーブル同士を関連付けて、簡単にデータ抽出できる。

```association.rb
require 'active_record'
require 'pp'
require 'logger'

Time.zone_default = Time.find_zone! 'Tokyo'
ActiveRecord::Base.default_timezone = :local
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Base.establish_connection(
  "adapter" => "sqlite3",
  "database" => "./myapp.db"
)

class User < ActiveRecord::Base
  # 複数のcommentsレコードを持つ(複数系)
  has_many :comments, dependent: :destroy
end

class Comment < ActiveRecord::Base
  # 1つのuserに所属する(単数系)
  belongs_to :user
end

User.delete_all

User.create(name: 'user1', age: 27)
User.create(name: 'user2', age: 27)
User.create(name: 'user3', age: 33)
User.create(name: 'user4', age: 20)

Comment.delete_all

Comment.create(body: 'comment1', user_id: 1)
Comment.create(body: 'comment2', user_id: 2)
Comment.create(body: 'comment3', user_id: 1)

# includes(:symbol)でAssociationデータも引き取れる
# Modelでhas_manyで定義した名称
user = User.includes(:comments).find(1)

pp user.comments.each do |c|
  puts "#{user.name}: #{c.body}"
end

# includes(:symbol)でAssociationデータも引き取れる
# Modelでbelogs_toで定義した名称
comments = Comment.includes(:user).all

comments.each do |c|
  # c.user.nameでAssociationのUserデータを取得できる
  puts "#{c.body} by #{c.user.name}"
end

# IDが1のUserを削除(Commentを削除)
User.find(1).destroy

```

```sql:import.sql
drop table if exists users;
create table users (
  id integer primary key,
  name text,
  age integer,
  created_at,
  updated_at
);

drop table if exists comments;
create table comments (
  id integer primary key,
  user_id integer,
  body text,
  created_at,
  updated_at
);

```

# 最後に

ActiveRecordは自分の中ではかなり分かりやすいO/Rマッパーだと思います。
(柔軟だし、個人的には命名規則がしっかりしている方が好き)

Railsとの相性も抜群なので、理解を深めて柔軟に使いこなしていきたいと思います。