n対nの関係で中間テーブルにid以外のカラムを持たせ登録・編集を行う方法
Ruby On Railsでのアソシエーションの応用のお話。
下記のようなUserモデルとGroupモデルにn対nの関係を持たせ、UserにGroupの所有者情報を持たせたい時、中間テーブルに所有者情報を持たせた方がDB設計的になんとなく綺麗な気がする。
しかし、通常中間テーブルのレコードには直接アクセス出来ないため、モデルのアソシエーションで一工夫する必要がある。
n対nのアソシエーション
通常のn対nの関係は、以下のように表現する。まずはschema.rbから、
db/schema.rb ActiveRecord::Schema.define(version: 2019_08_26_011656) do create_table "groups", force: :cascade do |t| t.string "name", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "users", force: :cascade do |t| t.string "name", null: false t.string "email", null: false t.string "password_digest", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "group_users", force: :cascade do |t| t.bigint "group_id" t.bigint "user_id" t.index ["group_id"], name: "index_group_users_on_group_id" t.index ["user_id"], name: "index_group_users_on_user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end
次にモデル。has_many
のthrough
オプションに中間テーブルのシンボルを渡す事でn対nを表現している。
app/models/group.rb class Group < ApplicationRecord has_many :group_users, dependent: :destroy has_many :users, through: :group_users end app/models/user.rb class User < ApplicationRecord has_many :tasks, dependent: :destroy has_many :group_users, dependent: :destroy end app/models/group_user.rb class GroupUser < ApplicationRecord belongs_to :group belongs_to :user end
中間テーブルにカラムを持たせる方法
上記で作成したGroup_userにGroupの所有者、つまりowner
を持たせたい。
その場合、Groupモデルに下記のように記述を加えていく。
app/models/group.rb class Group < ApplicationRecord has_many :group_users, dependent: :destroy has_many :users, through: :group_users has_one :owner_group_user, -> {where(owner: true)}, class_name: 'GroupUser' has_one :owner_user, through: :owner_group_user, source: :user end # migrationファイルも忘れずに実行する db/schema.rb ActiveRecord::Schema.define(version: 2019_08_26_011656) do create_table "group_users", force: :cascade do |t| # ~ 中略 ~ t.boolean "owner", default: false, null: false end end
中間テーブルであるowner_group_user
でownerがtrueなレコードを取得し、それを使ってUserモデルからowner_user
を取得している。
class_name: 'GroupUser'
で用いるモデルはGroupUserであることを明記し、source: :user
でUserモデルの情報を参照することを明記している。
これでViewで、
group.owner_user.name
と書くと、Groupの所有者名を取得することが出来る。
またController側でも、Viewから送られてくるparamsにowner_user
の値をmerge
すればそのままcreate
やupdate
にparamsを渡す事で、owner_user
を格納できる。
def group_params params.require(:group).permit( :name, { user_ids: [] } ).merge(owner_user: current_user) end
以上!
RailsでBootstrapのflashメッセージを良い感じに表示してみた
アプリケーションを作るさいに必ず多用するflashメッセージですが、これをBootstrapを使うと良い感じのデザインで作れます。
今回はhelperも使ってcssも切り替えながら、flashメッセージ部分を部分テンプレート化してみました。
コード
まずはflashメッセージのテンプレート部分
app/views/layouts/_flash.html.slim - flash.each do |key, value| div class=("alert #{flash_class_for(key)}") role="alert" button.close data-dismiss="alert" × = content_tag :div, value, class: key
次に共通レイアウト部分
app/views/layouts/application.html.slim = render partial: "layouts/flashes", flash: flash
最後にhelperを実装
app/helpers/application_helper.rb module ApplicationHelper def flash_class_for(key) case key when 'success' then 'alert-success' when 'failed' then 'alert-danger' end end end
以上で完成。
成果物
参考
Twitter:Bootstrap getbootstrap.com
FactoryBotで中間テーブルのテストデータを用意する方法
FactoryBotでテストデータの中間テーブルを用意する方法がわからず、少し詰まったのでメモ。
想定
想定としてUserモデルとGroupモデルが存在し、n対nの関係であるとする。 中間テーブルのモデル名はGroup_userとする。
app/models/user.rb class User < ApplicationRecord has_many :group_users, dependent: :destroy has_many :groups, through: :group_users end app/models/group.rb class Group < ApplicationRecord has_many :group_users, dependent: :destroy has_many :users, through: :group_users end app/models/group_user.rb class GroupUser < ApplicationRecord belongs_to :group belongs_to :user end
FactoryBotの記述方法
レコードの関連付けを行う場合は色々な書き方があるが、ここではその一例を紹介する。 基本的には関連付けはtraitとcallbackを用いて記述する。
spec/factories/user.rb FactoryBot.define do factory :user do sequence(:name) { |n| "test_user#{n}" } sequence(:email) { |n| "test_email#{n}@example.com" } sequence(:password) { "password" } trait :user_with_groups do after(:build) do |user| # after(:build)とした場合、createの場合もcallbackが走る user.groups << build(:group) end end end end spec/factories/group.rb FactoryBot.define do factory :group do equence(:name) { |n| "test_user#{n}" } end end
上記の書き方をしておくと、userデータを作成後、traitでuserに紐づくgroupの作成をするかしないかを切り替える事が出来る。
# Userに紐づくGroupを作成する場合 user = create(:user, :user_with_groups) # Use単体を作成する場合 user = create(:user)
これでUserに紐づくGroupの作成・非作成を切り替える事が出来た。
seedデータってどうやって入れる?seed-fuを使った便利なSeedデータ挿入法
railsアプリの初期データを入れ方として初期段階ではrake db:seed
があります。
でも、繰り返し使った場合同じデータが登録されたり、登録するデータの選別が出来ません。(コメントアウトを使えば出来るけどめんどくさくてやってられない)
ここでseed-fuを使えば、未登録のデータだけ登録したり開発環境と本番環境のデータを分ける事も可能です。
インストール
rails5.0系以上では2.3を使います。
Gemfile gem 'seed-fu', '~> 2.3'
ディレクトリ配置
seed_fuではdb直下にfixturesディレクトリを作成し、その下にseedファイル、もしくはディレクトリを配置します。 例えば開発環境と本番環境でのseedファイルを分けたい場合、下記の様に環境ごとにseedファイルを分ける事が出来ます。
db ├ fixtures ├ development ├ ○○.rb ├ ○○.rb ├ production ├ ○○.rb ├ ○○.rb ├ migrate
ファイルの実行
seedファイルが用意出来たと想定し、ファイルを実行しseedデータを入れていきます。(seedファイルの書き方は後述) rakeタスク実行時、FIXTURE_PATHオプションを付与する事でseedファイルを読み込むパスを指定できます。 また、FILTERオプションを付与する事でseedファイルを指定する事も出来ます。
# 通常の実行 rake db:seed_fu # ファイルパスを指定する場合 rake db:seed_fu FIXTURE_PATH=path/to/fixtures # seedファイルを指定する場合 rake db:seed_fu FILTER=users,articles # 複数指定する場合はカンマ区切り
seedファイルの記述の仕方
seedファイルの書き方は下記のようになる。下記を実行するとデータが1つ挿入される。
db/fixtures/user.rb User.seed do |s| s.name = "田中健太郎" s.email = "aaa@gmail.com" s.password = "123456" end
しかし、実際は大量のデータを扱う事になるので、下記のように記述する事が多いかもしれない。 下記を実行するとデータが10個挿入される。
db/fixtures/user.rb 10.times do |n| User.seed do |s| s.name = "テスト名前#{n}" s.email = "aaa#{n}@gmail.com" s.password = "123456" end end
seedデータ実行順
seedデータは上から順に実行されるので、モデルをネストさせている場合ファイル名通りに読み込まれ途中でエラーを吐く可能性がある。 そこで、一番上のモデルからファイル名の頭にナンバリングしておくと良い。
db ├ fixtures ├ 01_user.rb ├ 02_group.rb ├ 03_task.rb ├ migrate
こうする事で、ナンバリングされた順でファイルが読み込まれるため関連付けされていない事によるエラーが起こらない。
似ているようで実は違う!?dependent: :destroyとdependent: :delete_all
親モデルに紐付く子モデルがある場合、has_many:
で関連付けしただけだと親モデルを削除すると子モデルが孤立してしまうため、通常はdependentオプションでdestroyを指定し、親モデルを削除するとそれに紐付く子モデルも削除されるように設定する。
しかし、実は子モデルを削除するには:destory
だけでなく:delete_all
も存在する。
例
ここにUserモデルに紐付くTaskモデルが存在するとする。
app/models/User.rb class User < ApplicationRecord has_many :tasks, dependent: :destroy # dependent: :destroyを使用 has_many :tasks, dependent: :delete_all # dependent: :delete_allを使用 end
app/models/Task.rb belongs_to :user # ここではoptionは指定しない end
上記のように、親モデルのhas_many
のオプションとしてdependent:
を指定する。
:destroy
を指定した場合、子モデルの削除はActiveRecordを介してSQL文が発行されるのに対し、:delete_all
を指定した場合はActiveRecordを介さず直接SQL文が発行される。
発行されるSQL文
例えばid: 1のUserにid: 1,2,3,4,5の5つのTask紐付いている場合
:destroyの場合 # Userを選択 SELECT "tasks".* FROM "tasks" WHERE "tasks"."user_id" = $1 [["user_id", 1]] # Taskをuser_idから一つずつ選択し、削除 SELECT "tasks".* FROM "tasks" WHERE "tasks"."user_id" = $1 [["user_id", 1]] DELETE FROM "tasks" WHERE "tasks"."id" = $1 [["id", 1]] SELECT "tasks".* FROM "tasks" WHERE "tasks"."user_id" = $1 [["user_id", 1]] DELETE FROM "tasks" WHERE "tasks"."id" = $1 [["id", 2]] SELECT "tasks".* FROM "tasks" WHERE "tasks"."user_id" = $1 [["user_id", 1]] DELETE FROM "tasks" WHERE "tasks"."id" = $1 [["id", 3]] SELECT "tasks".* FROM "tasks" WHERE "tasks"."user_id" = $1 [["user_id", 1]] DELETE FROM "tasks" WHERE "tasks"."id" = $1 [["id", 4]] SELECT "tasks".* FROM "tasks" WHERE "tasks"."user_id" = $1 [["user_id", 1]] DELETE FROM "tasks" WHERE "tasks"."id" = $1 [["id", 5]] # 最後にUserを削除 DELETE FROM "users" WHERE "users"."id" = $1 [["user_id", 1]]
delete_allの場合 # Taskをuser_idから一括削除 DELETE FROM "tasks" WHERE "tasks"."user_id" = $1 [["user_id", 1]] # Userを削除 DELETE FROM "users" WHERE "users"."id" = $1 [["id", 1]]
上記のように、:destroy
の場合はuser_id
から一つずつtaskを選択して削除しているのに対し、:delete_all
ではuser_id
から一括してtaskを削除している。
大量のデータを扱う場合を想定しているなら、:delete_all
を使った方が良さそう。
【ActiveAdminにご用心】ActiveAdmin導入時のassetファイル汚染防止法
管理画面を簡単に作ることの出来るactiveadminですが、そのまま何も考えずに導入して通常ページのデザインが崩れてしまいました。
原因
これはactiveadminのCSSやJavascriptが管理画面以外でも読み込まれる事が原因です。
rails generate active_admin:install
を実行した際、app/assets/javascripts/active_admin.js
とapp/assets/stylesheets/active_admin.css
が生成されますが、こいつらをapplication.js
とapplication.css
が読み込んじゃってます。
なので、application.js
とapplication.css
ではactiveadmin系のassetファイルを読み込まないよう設定します。
対策
①まず、下記のようなフォルダ構成にします。
javascripts ├ admin ├ active_admin.js.coffee ├ main ├ その他のjavascriptファイル ├ application.js stylesheets ├ admin ├ active_admin.css ├ main ├ その他のcssファイル ├ application.css
②次に、jsとcssのapplicationファイルの//= require_tree .
を、//= require_tree ./main
に変更します。
③続いて、下記を追加します。
config/initializer/active_admin.rb config.clear_stylesheets! config.register_stylesheet 'admin/active_admin.css' config.clear_javascripts! config.register_javascript 'admin/active_admin.js'
Herokuを使用している場合は下記も追加します。
config/application.rb config.assets.precompile += %w[admin/active_admin.css admin/active_admin.js]
④最後に、サーバを再起動してassetファイルがきちんと読み込まれていたら完了です。
LaravelでSQLiteを使う際のmigrationの忘備録
LaravelでDBをSQLite3を使った時に最初のDB設定でハマったので忘備録。
LaravelのデフォルトのDBはMYSQL
LaravelではデフォルトのDBがMYSQLなので、それ以外のDBを使う際は設定を変えないといけない。
/.env
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret
デフォルトでこうなっているので、以下に変更。
/.env
DB_CONNECTION=sqlite
sqliteデータベースの作成
次に、ルートディレクトリ/databaseディレクトリの中にdatabase.sqliteファイルを作成
// アプリケーションのルートにいる状態で touch database/database.sqlite
これは/config/database.phpファイル内で、sqliteの設定箇所にdatabase.sqliteという名前で設定されているため。
/config/database.php
'sqlite' => [ 'driver' => 'sqlite', 'url' => env('DATABASE_URL'), 'database' => env('DB_DATABASE', database_path('database.sqlite')), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), ],
最後にmigrateを実行
ここまで出来ればmigrationを実行して完了。
php artisan migrate