プログラミングのメモ帳

自分のメモとして残したいアウトプット

いいね機能のDB設計

よくある「いいね機能」のDB設計を考える。
ツイッターで言えばいわゆる「ファボ」機能。

前提条件

・UserモデルとPostモデルが存在する
・User has_many posts という関連付けが行われている

f:id:d4192:20190416190444p:plain ER図

Like Modelを作る

Postにいいねしたユーザー一覧を入れるという力技も出来そうですが
SQLアンチパターンで言う所の「ジェイウォーク」になりそうです。
カラムに複数の値を入れると将来運用上のデメリットしかありません。
なので定石ではありますが新しくLikeモデルを作成し、
1レコード=1つのLike
を表して、1カラムに1つの値だけが入るようにします。

f:id:d4192:20190416191124p:plain

外部キーを設定してLIkeモデルは必ずUser, Postと整合性が保たれるようにします。

$ rails g model Like user:references post:references
$ rais db:migrate

レコードの挿入と削除

あとはいつも通りActiveRecordを使ってSQLを発行するだけ。
ただLike専用のビューは作らないと思うのでuserやpostのビューから
効率的にアクセスする為に関連付けが必要

#models/user.rb
has_many :likes, dependent: :destroy
#models/post.rb
has_many :posts, dependent: :destroy

これで

User.find(1).posts.find(1).likes.count

などをしてlikeモデルにアクセスできる。
また、User, Postどちらが削除された時に紐付くLikeも削除されます。

一意性のバリデーション

Likeはuser_id属性とpost_id属性を持ちますが、
この2つの組み合わせは常に一意(unique)です。
なのでこの組み合わせに対して一意性のバリデーションを設定しておきます。

#models/like.rb
validates :user_id, uniqueness: { scope: :post_id }

なお、このバリデーションのエラーメッセージは通常のuniquenessのままなので
状況に合わせてmessageオプションで変更しておきましょう。

throughオプションを使う。

f:id:d4192:20190416191124p:plain

こちらの図のような関連付けが行われている時に
「あるuserがlikeしたpostを抽出したい」という場面があると思います。
そんな時は
1.Userに紐付くLikeを抽出し、selectメソッドを利用してpost_idだけを抜き出す
2.Post.where(post_id: ? )に1の結果を入れる

といったようなややこしいことをしないといけません。
そこでthroughオプションを使って簡潔にアクセスできるように変更します。

#/app/models/user.rb
has_many :liked_posts, through: :likes, source: :post

上記の記述でlikesを通してpostへアクセスする
liked_postsというメソッドが出来上がります。
これで新しいメソッドliked_postsが使えます。

@user.liked_posts

とするだけでuserがlikeしたpostオブジェクトが返ってきます。

Controllerへの実装

controllerへの実装は他のモデルと同じようにすれば大丈夫ですね。
ルーティングはlikes#createとlikes#destoryだけで良いのではないでしょうか。

Userモデルに紐付くModelの作り方

かなり基本の部類に入るとは思うがUserモデルに紐づくModel
(例えばPostだったりTaskだったりTweetだったりする)
の作り方を確認してみる

Modelの生成

まずはgenerate(カラムはお好み)

$ rails g Model Post title:string content:text user:references

user:referencesをつけることでmigrationファイルに

t.references :user, foreign_key: true

が追記されます。
さらにmodels/post.rbに

belongs_to :user

が追記されています。
userテーブルに対して外部キーが設定されますのでPostとUserの整合性が保たれます。
また、下記のようにインデックスを貼っておくのが定石のようです。

#user_idとcreated_atに対してインデックスを追加
add_index :posts, [:user_id, :created_at]

Migrationとmodels/user.rbの編集

そしてmigration

$ rails db:migrate

またuser.rb側でも関連付けを行います。
(dependent: :destoryは親を削除した時に関連する子も削除する設定)

has_many :posts, dependent: :destroy

これによりUserのインスタンスからPostモデルにアクセスできるようになりましたので
user_controllerでは関連付けを利用してモデルへアクセスしましょう。

特定のvalidationをスキップする

WEBサービスを作っている際に、
「passwordのバリデーションが邪魔だな、、、、」と思う場面がありました。

具体的にはユーザーのプロフィールを変更する時にパスワードを入力せずに更新したい、 つまりパスワードのバリデーションのみ適用無しにしたい、というケースでした。 いくつか方法があるようです。

onオプションを使用する

onオプションを使用すれば特定のアクションにのみバリデーションを適用できます。

#例(createアクションのみにpresenceが適用される)
validates :name, presence: true, on: :create

before_actionやrouteのonlyオプションのイメージですね

update_columnsを使う(非推奨)

バリデーションをそもそも行わないupdate_columnsを使う。
しかし全てのカラムからvalidationが外れてしまうので非推奨。 カラム1つだけの更新であればupdate_columnを使う手もありますが
今回はpassword以外の複数カラムを更新したいので使えません。

Railsのメッセージを日本語化する

Railsで表示されるメッセージ(エラーや曜日等)はもちろんデフォルトで英語です。
これらのRailsで用意されているメッセージを日本語化する。
I18nRails自体に同梱されているのでGemfileに追記は不要。

流れとしては
1. locale: ja 用の翻訳ファイルをダウンロードする(または作成)
2. デフォルトのlocaleをjaに設定する。

日本語翻訳ファイルを手に入れる

翻訳の元となるファイルをconfig/localesに配置する必要があります。
自分で作成しても良いですが既に作成されたものがあるので
ダウンロードしてconfi/localesに配置しましょう。

DL先:https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml

デフォルトのlocaleを :ja に設定する

config/initializer/locale.rbを作成し以下に変更

I18n.config.available_locales = [:en, :ja]  
I18n.default_locale = :ja  

localesのホワイトリストを:enと:ja、デフォルトを:jaに設定

Active Recordに翻訳ファイルを対応させる

以下Rails Guideに掲載されているサンプルコード

en:
  activerecord:
    models:
      user: Dude
    attributes:
      user:
        login: "Handle"
      # "login"属性は"Handle"という語に翻訳される

ActiveRecordに対応させるには上記のように
modelsにテーブル名、attributes以下にカラム名の翻訳情報を書く。

model_name.humanメソッド、human_attribute_nameメソッドを使って
それぞれの翻訳情報にアクセスできる

User.model_name.human #=>"Dude"
User.human_attribute_name(:login) #=>"Handle"

参考リンク

Rails国際化 (I18n) API - Rails ガイド

コミットメッセージの良い習慣を身につける

コミットメッセージの良い習慣を身につける

チーム開発を前提として他の人が見たとき、または未来の自分が見た時に
分かりやすいコミットメッセージをつけるべきである。

基本としては下記の3点を抑える

・3行構成でコミットメッセージ書く

・内容は「更新内容」+「空行」+「更新理由」

・タイトルにはPrefixをつける

3行構成でコミットメッセージを書く

コミットメッセージは複数行で書くのが基本。

git commit -m 'message'

と mオプションを指定している場合は1行のコミットメッセージになりますが
-mを指定しない場合はvimが起動して複数行の記入が可能です。

また

git commit -m '1行目' -m '2行目' -m '3行目'

と指定することで複数行のメッセージを書くことも可能。

「更新内容」+「空行」+「更新理由」の構成

3行のコミットメッセージは更新内容、空行、更新理由という構成にします。

$ git commit
[prefix] title

body

titleには「何をしたか」が具体的に分かるように。
bodyには「なぜそうしたか」という理由が分かるように書く。

titleだけで具体的に伝われば一番良いと思う。

ちなみに2行目の空行が無くてもGitHubの表示に問題は無かった。
しかしlogを取ったりする時に上手く行かない場合があるらしいのでしっかり空行は入れたほうが吉。

Prefixをつけよう

コミットメッセージにはPrefixをつけよう。 例えば、
「post_controllerを追加」 なら
[Add]post_controllerを追加
という形。 メールで件名の頭に【報告】だったり【依頼】とつけるようなイメージ。

これは開発チームで規約化するので世界標準でカチッと決まっているわけではない。
それぞれのプロジェクトに沿って運用する。

また、Prefixをビジュアルとして表現するEmoji_Prefixを使っている人も見かける。

参考リンク

GitHubのコミットメッセージに絵文字を入れて開発効率をあげる - Qiita
Emojiで楽しく綺麗なコミットを手に入れる | Goodpatch Blog
GitのコミットメッセージにEmoji Prefixを使ってテンションをあげたい話🕺💃🕺💃 - LiBz Tech Blog
Gitのコミットメッセージの書き方 - Qiita

Railsで検索フォームを実装する(ransack)

Ruby on Railsで検索フォームを実装する方法。

ransackというgemを使って実装できる。
ちなみにransackは「くまなく探す」という意味らしい。凄い検索してくれそう。

githubレポジトリはこちら(https://github.com/activerecord-hackery/ransack)

gem 'ran sack'

$ bundle

Controllerの記述は

def index
    @q = User.ransack(params[:q])
    @users = @q.result(distinct: true)
end

のような形になります。
ちなみにdistinctをつけることでSQL
SELECT hoghoge FROM ~
から
SLECT DISTINCT hogehoge FROM ~
に変わります。(重複が無くなる)

Viewの方ではransackを入れたことにより
search_form_forヘルパーが使用できます。
これを使ってフォームエリアを作ります。

<%= search_form_for @q do |f| %>
    <%= f.label :name_cont %>
    <%= f.search_field :name_cont %>
    <%= f.submit %>
<%= end %>

フィールドの :name_cont は 「:nameカラムに対してcontで検索する」という意味になります。
contはSQLのLIKE句と同じ意味になります。
ここら辺のmatcherについては公式レポジトリのMatcher項に記載されています。
このようにransackはviewのmatcherとcontrollerのresultをカスタムして使う。

Railsルーティングのあれこれ(routes.rb)

Railsのルーティングの指定方法に色々種類があったので 自分用に整理する

基本形

get '/login', to: 'sessions#new'

httpメソッド 'パス', to: 'コントローラー#アクション'
という書き方。

CRUD機能を一括で指定

resources :users

resources :コントローラー名
CRUDの各機能を満たすルーティングが一括で指定される。

上記の例でいうと

get '/users',              to: 'users#index' 
get '/users/:id',         to: 'users#show'
get '/users/new',      to: 'users#new'
post '/users',            to: 'users#create'
get '/users/:id/edit', to: 'users#edit'
put '/users/:id',        to:  'users#update'
delete '/users/:id,    to: 'users#destroy'        

の7つのルーティングが一括で指定される。 基本的な機能は一発でルーティングされるがURLパターンを変更したい場合(login等)は個別に指定するべし

resourceから特定の機能のみルーティング

resourcesメソッドの第二引数に'only: []'をつけることで特定のアクションのみ実装する

#indexとshowアクションのみにルーティング
resources :users, only: [:index, :show]

namespace(名前空間)を指定する

Controllerに名前空間が指定されている場合、routes.rbにもnamespaceを指定する。

#admin/posts_controllerを指定する場合
namespace :admin do
  get '/users', to: 'users#index
end

上記のroutesは

'admin/users', to: 'admin/users#index

と同じになる。

scopeを使って柔軟に指定する

URLパターンだけ一括で変更したい、逆にコントローラーだけ一括で指定したい。
という場合にはscopeを使う。

scope :admin do
  get '/users', to: 'users#index 
end

これは

  get 'admin/users', to: 'users#index'

を同じになる。(URLパターンだけ名前空間が付与される)

scope module: 'admin' do
  get '/users', to: 'users#index 
end

  get 'users', to: 'admin/users#index'

と同じ。(コントローラーにだけ名前空間が付与される。)