ActiveRecord
Model定義系
クラス名とテーブル名の関係
クラス名に名前空間を含んだとしても、テーブル名は変わらない(基本的にテーブル名は名前空間を含まない)
関連付けをすると、自身のクラス名を名前空間としたクラス名で参照しに行く?
ActiveRecordバリデーション
code:rb
# 基本系
validates :name, presence: true
# 空欄を許可する (allow_blank)
validates :sbr_account_classification_code, inclusion: { in: %w(01 02), message: "には無形または雑費を入力してください", allow_blank: true }
validates :no_of_days, format: {with: /\A0-9+\z/, allow_blank: true} # 数字のみ許可
validates :system_no, length: { maximum: 20 }, format: { with: /\A\d+\z/ }
フォーマット判定いろいろ
一般的には行頭行末は^と$なんだけど、
^と$は脆弱性があるらしく、\Aと\zに変えるようRailsからお達しがある。
code:rb
# 数字
/^ー?0-9+(\.0-9+)?$/; /^-?0-9+(\.0-9+)?$/ # 全て全角数値(マイナス、小数点);全て半角数値(マイナス、小数点) # 文字
/^a-z+$/; /^A-Z+$/; /^a-zA-Z+$/ # 半角アルファベット(小文字; 大文字; 大文字・小文字) /\A(?:\p{Hiragana}|\p{Katakana}|ー-|一-龠々)+\z/# 全角ひらがな、全角カタカナ、漢字(鬼車) /^a{1,5}$/ # 1〜5文字
# 郵便番号
/^\d{3}-\d{4}$|^\d{3}-\d{2}$|^\d{3}$/# 郵便番号(ハイフンあり3桁・5桁・7桁) /^\d{3}-\d{2}$/; /^\d{3}-\d{4}$/ # 郵便番号(ハイフンあり5桁; ハイフンあり7桁) /^\d{3}$/; /^\d{5}$/; /^\d{7}$/ # 郵便番号(ハイフンなし3桁; 5桁; 7桁)
/^\d{3}-\d{4}$|^\d{3}-\d{2}$|^\d{3}$|^\d{5}$|^\d{7}$/# 郵便番号(ハイフンあり・なし両方) # その他
/^\d{10}$/; /^\d{11}$/; /^\d{10}$|^\d{11}$/ # 電話番号(ハイフンなし10桁; 11桁; 10桁or11桁)
/^\S+@\S+\.\S+$/# e-mail
正規表現でOR
hoge|fuga
バリデーションエラーメッセージの定義場所
バリデーション実行タイミングの指定
ビルトインタイミングはcreateとupdate
code:rb
class Person < ApplicationRecord
# 値が重複していてもemailを更新できる
validates :email, uniqueness: true, on: :create
# 新規レコード作成時に、数字でない年齢表現を使用できる
validates :age, numericality: true, on: :update
# デフォルト (作成時と更新時のどちらの場合にもバリデーションを行なう)
validates :name, presence: true
end
カスタムコンテキストを使う場合(任意タイミングの場合)
code:rb
class Person < ApplicationRecord
validates :email, uniqueness: true, on: :account_setup
validates :age, numericality: true, on: :account_setup
end
person = Person.new
この場合、トリガーになるのは次の通り
code:rb
person.valid?(:account_setup)
person.save(context: :account_setup)
まとめて定義したい時は
code:rb
with_options on: :registration do |registration|
registration.validates :agreement, acceptance: true # 同意チェックのバリデーション
registration.validates :first_name_kana, :last_name_kana, format: { with: /\p{katakana}ー-+/, message: 'カタカナで入力して下さい。', allow_blank: true } registration.validates :first_name, presence: true
end
ActiveRecord関連付けの書き方
アソシエーション
code:rb
# 自分が相手を知っている
class Book < ApplicationRecord
belongs_to :author # 基本
belongs_to :parent_id, class_name: "AccountItem" # 外部キーとModel名が一致しない時
belongs_to :account_item, optional: true # 任意入力の場合(デフォルトは必須になってしまう)
belongs_to :organization, dependent: :destroy # 自分が消えた時、相手も消す
end
# 相手が自分を知っている
class Author < ApplicationRecord
has_many :books
end
# 多対多関連(間に連結テーブルが挟まる形)
class User < ApplicationRecord
has_many :user_roles
has_many :roles, through: :user_roles # user_rolesを中継して関連
end
関連づけた子Modelの生成
build_*はsaveしない。create_*はsaveする。
belongs_to :author
model.build_author
mdel.create_auther
has_one :user
model.build_user
model.create_user
has_many :books
model.books.build([{a: :A, b: :B}, {a: a, b: :b}])
model.books.create([{a: :A, b: :B}, {a: a, b: :b}])
関連付けの取得
code:rb
# 全部
Task.reflect_on_all_associations
# belongs_to だけ
Task.reflect_on_all_associations(:belongs_to)
# has_many だけ
Task.reflect_on_all_associations(:has_many)
ActiveRecord_Relationからアソシエーション関係の定義を取得
divi.reflect_on_association(:sic_department).klass.human_attribute_name('sic_department_code')
キモはreflect_on_association
rails独自クラス謎挙動
model配下にピュアなRubyClassを配置した時、
名前空間とフォルダ構成を一致させておかないと、Railsが認識しない模様。
model/hoge/fuga.rbの場合は
class Hoge::Fugaにする
Modelにenumを定義
持たせたカラムと同じ名前でenumを定義できる
code:rb
# migration
add_column :user, :role
# model
enum role: { admin: 1, member: 2 }
こうしておくと、user.admin?でその値が設定されているかどうか確認できる。マジックナンバーが消えるよ!
ただし、formには実データじゃなくてラベルを値としてやり取りする事
code:erb
<% collection = User.human_attribute_name(:member), :member], [User.human_attribute_name(:admin), :admin%>
<%= f.input_field :role, class: "form-control", as: :select, collection: collection, include_blank: false %>
enumに渡すHashをstringで定義
スペースが含まれている場合は、user.IS Staff?みたいな呼び方はできないが、user.send('IS Staff?')みたいな事はできる。
Modelの取りうる値をenumで宣言した時に、日本語化するgem
汎用gemfileに入れておきたい
boolean型のカラムを追加した時に使えるもの
code:rb
add_column :users, :admin, :boolean, default: true #として user.admin? #=> true || false user.toggle!(:admin) # adminが反転する
Model更新系(save, update)
saveにおける戻り値にバリエーション
成功した場合、true
バリデーションに失敗した場合、false
その他の例外の場合、エラースロー
createにおける戻り値のバリエーション
成功した場合、createしたインスタンス(id割り当て)
バリデーションに失敗した場合、errorsに値が入った状態のインスタンス(id未割り当て)
その他例外の場合、エラースロー
saveの場合バリデーションに失敗するとfalseが返るのでごっちゃになる
すでに保存済みのModelかどうか検査
model.persisted?
逆はmodel.new_record?...こっちをメインにした方が良いのでは
複数レコードの一括更新
View
code:haml
=form_tag post_path, method: :post
-@records.each do |item|
=fields_for 'items[]', item do |f|
=f.text_field :email
=f.text?field :name
end
end
取り出す時
code:rb
# => {"4"=>{"name"=>"pencil", "price"=>"120"},
# "5"=>{"name"=>"eraser", "price"=>"110"},
# "6"=>{"name"=>"sellotape", "price"=>"110"}}
Model
code:rb
User.where("created_at >= ?", Date.new(2017))
.update_all("point = point + 100")
複数のDBテーブルのレコードを一気に作る時のメモ
form objectの考え方でやってみた感じ
code:rb
def create_or_update
ActiveRecord::Base.transaction do
if id.blank?
partner_code = Partner.generate_partner_code
@partner = Partner.new(
partner_code: partner_code,
partner_name: partner_name
)
# ここでsave!してしまうと、子レコードのバリデーションが行われないため
# バリデーションだけ実施
@partner.valid?
@partner_contract = PartnerContract.new(
man_hour: man_hour,
contract_price: contract_price,
start_date: start_date,
end_date: end_date,
partner_suppliers: partner_suppliers,
sic_department_id: sic_department_id,
sic_division_id: sic_division_id,
)
@partner_contract.valid?
@partner.save!
# 親子の紐付けにはidが採番されていないといけないので、セーブしてから投入する
@partner_contract.partner = @partner
@partner_contract.save!
else
@partner = Partner.find(id)
@partner_contract = @partner.partner_contracts.first
@partner.partner_name = partner_name
@partner.valid?
@partner_contract.man_hour = man_hour
@partner_contract.contract_price = contract_price
@partner_contract.start_date = start_date
@partner_contract.end_date = end_date
@partner_contract.partner_suppliers = partner_suppliers
@partner_contract.sic_department_id = sic_department_id
@partner_contract.sic_division_id = sic_division_id
@partner_contract.valid?
@partner.save!
@partner_contract.save!
end
end
return @partner
rescue => e
import_errors
p e
p errors
return nil
end
private
# 取り扱うModelで発生したエラーを自分に取り込んで、表示する時にまとめて表示するようにする
def import_errors
@partner.errors.each do |attr, error|
errors.add attr, error
end
@partner_contract.errors.each do |attr, error|
errors.add attr, error
end
end
Formsフォルダ配下にソース置いたら、やっぱりForms名前空間に勝手に配置されるぽい
どうも、ソースファイルの検索先はModels直下とModels/Concernsの二つしか定義されていないようで
Conserns以外のフォルダは名前空間扱いになるぽい
code:rb
class Forms::FormObject
include ActiveModel::Model
attr_accessor :hoge, :fuga
validates :hoge, presence: true
validates :fuga, presence: true
def create
Hoge.transaction do
hoge = Hoge.new(hoge: hoge)
import_errors(hoge) and return nil unless hoge.save
fuga = Fuga.new(fuga: fuga)
fuga.hoge = hoge
import_errors(fuga) and return nil unless fuga.save
end
end
private
def import_errors(model)
model.errors.each do |attr, error|
errors.add attr, error
end
end
end
こんな感じ
落とし穴として、FormObjectでバリデーションしても、UniqueなどはActiveRecordでしかバリデーションできない。
なので、連携先のActiveRecordが吐いたerrorsを吸い上げる仕組みが必要
バリデーションをActiveRecordに一任すると、一括のバリデーションにならないのでFormObjectでもバリデーションしてる。(見栄えのために
Model検索系(select)
code:rb
# 2つのカラムでソートだけど、名前だけ降順
User.order({ name: :desc }, :email)
# 今日投稿された Post を取得
scope :created_today, -> { where("created_at >= ?", Time.zone.now.beginning_of_day) }
# または
scope :created_today, -> { where(created_at: Time.zone.now.all_day) }
# 昨日投稿された Post を取得
scope :created_yesterday, -> { where(created_at: 1.day.ago.all_day) }
# 3日前に投稿された Post を取得
scope :created_three_days_ago, -> { where(created_at: 3.days.ago.all_day) }
# 一週間前に投稿された Post を取得
scope :created_a_week_ago, -> { where(created_at: 1.week.ago.all_day) }
# 一ヶ月前に投稿された Post を取得
scope :created_a_month_ago, -> { where(created_at: 1.month.ago.all_day) }
# 2日前から昨日まで
User.where(confirmation_sent_at: 2.days.ago.midnight..Time.now.midnight)
# 紐づけたテーブルを使って検索するとき
Article.includes(:comments).where(comments: { visible: true })
# 入れ子のmodelをinclude
Ranking.includes(article: :author) # ranking.article.authorみたいなときの話。
# 実行計画の確認
User.all.explain
# 部分一致
@projects.where("service_code like ? ", '%%%s%%' % params:service_code) # 最大値取得
User.maximum(:id)
# 該当レコードの削除
User.where(target_date: records0.target_date).delete_all # 重複の排除
User.where(target_date: records0.target_date).distinct OR検索
code:rb
relation = Character.joins(:anime)
relation
.merge(Anime.where(title: '刀語'))
.or(relation.where(sex: :male)).distinct
SQLの直接実行
code:rb
con = ActiveRecord::Base.connection
con.execute("INSERT INTO users(name, email) VALUES('Joe', 'joe@example.com')")
ただし戻り値も生なのでSelectに使うには取り回しにくい
こっちならHashで受け取れる
code:rb
con = ActiveRecord::Base.connection
result = con.select_all('SELECT name, email FROM users')
result.to_hash
# => [ {"name" => "Joe", "email" => "joe@example.com"},
# {"name" => "Alice", "email" => "alice@example.com"},
# {"name" => "Bob", "email" => "bob@example.com"} ]
プレースホルダ使う場合はこんな感じ
code:rb
sql = ActiveRecord::Base.send(
:sanitize_sql_array,
)
# => "SELECT * from users WHERE name='Bob\\'s'"
sql = ActiveRecord::Base.send(
:sanitize_sql_array,
[ 'SELECT * from users WHERE name=:name AND email=:email',
name: 'Bob', email: 'bob@example.com' ]
)
# => "SELECT * from users WHERE name='Bob' AND email='bob@example.com'"
rails クエリの貼り直し
model.rewhere
これでデフォルト条件を設定しやすくなる
Join関係
Transaction, ロック周り
レコードのロックをとる
採番テーブル法を使う時など
RailsのActiveRecordでtransactionの話
デフォルトの挙動ではいくつtransactionをネストさせても、一つのトランザクションであるかのように振る舞う
ネストした内側のトランザクションだけロールバックしたい時は、トランザクションの呼び方を変える
code:rb
User.transaction do
User.create(username: 'Kotori')
User.transaction(requires_new: true) do
User.create(username: 'Nemu')
raise ActiveRecord::Rollback
end
end
レコードのコピー
code:rb
def copy
@old_entry = Entry.find(params:id) @entry = Entry.new
@entry.attributes = @old_entry.attributes
render :action => "new"
end
ActiveRecordの結果を、属性を元にハッシュ化したい
employee_records.index_by(&:emoployee_code)
activerecordを直接使ってDBアクセス
code:rb
db_conf = YAML.load( ERB.new( File.read("./config/database.yml") ).result )
ActiveRecord::Base.time_zone_aware_attributes = true
DBConnection = ActiveRecord::Base.connection
rails modelのコールバックタイミング一覧
各タイミングの用途などは
Modelから属性の一覧
モデル名.column_names
ruby bigdecimal
floatはしょっちゅう誤差が出るので
ちゃんと少数を使う場合はbigdecimalを使え
DBに存在しない属性をModelに定義する
attr_accessor :tel
virtual attributeと呼ぶらしい
ただ、これだけだと[]を使ったアクセスができなかったりするので、ちょっと手入れが必要かも
こんなのを追加
code:rb
attr_accessor :job_category, :specialized, :level, :belongs
def []=(key, value)
if key.to_s == 'job_category'
self.job_category = value
elsif key.to_s == 'specialized'
self.specialized = value
elsif key.to_s == 'level'
self.level = value
elsif key.to_s == 'belongs'
self.belongs = value
else
super
end
end
def self.column_names
end
もっとシンプルにできんかなー
ActiveModel::Attributes
Rails5.2の新機能、ActiveMoelに対して属性の型を定義できるようにする
Rails5.1以下はgemで対応
子テーブルに対する増減をDBに反映させる仕組み
隠し機能が過ぎる
DBの使い方-事前読み込み
たくさん新規レコードを作った後、関連のあるいくつかのテーブルから値を参照する必要がある時。
普通に作ると、各レコード個別にリクエストが飛ぶので遅くなる(N+1の一種な気がする)
作った新規レコードから、各テーブルの関連IDの配列を抽出して、IN句で事前にロードする
code:rb
def self._after_new(records)
employee_codes = records.collect{|v| v.employee_code }
employee_records = Employee.includes(:sic_department,
:sic_division,
:sic_team,
:talent_class) \
.where(employee_code: employee_codes)
employees = {}
employee_records.each do |rec|
end
records.each do |record|
# puts 'insert employee values'
if employee.present?
record.employee_name = employee.employee_name
record.sic_department_code = employee.sic_department.sic_department_code
record.sic_department_name = employee.sic_department.sic_department_name
record.sic_division_code = employee.sic_division.try! :sic_division_code
record.sic_division_name = employee.sic_division.try! :sic_division_name
record.sic_team_code = employee.sic_team.try! :sic_team_code
record.sic_team_name = employee.sic_team.try! :sic_team_name
record.talent_class_code = employee.talent_class.talent_class_code
record.talent_class_name = employee.talent_class.talent_class_name
else
record.errors.add("要員コードが", I18n.t('import.invalid_format'))
end
# puts 'unified date'
if record.target_date != records0.target_date record.errors.add("対象年月", I18n.t('import.target_date_error'))
end
end
records
end
レコードを引き当てられるように辞書化してるのなんとかならんかな
=>employee_records.map(&:employee_code).zip(employee_records).to_hでいける気がする
=>employee_records.index_by(&:emoployee_code)でできることが判明・・・うおおお
カラム名に予約語(classなど)を使った場合
NoMethodError: undefined method fetch_value for nil:NilClass
ってエラーが出る
Railsモデルの中でURLヘルパーを使う
path = Rails.application.routes.url_helpers.samples_path
ActiveRecordでランダムにレコードを選ぶ
SomeModel.find( SomeModel.pluck(:id).sample )
RailsでModelのinitializeに何か書きたくなった時は
after_initializeを検討する