おぴよの気まぐれ日記

おぴよの気まぐれ日記

岡山やプログラミング、ファッションのこと、子育てや人生、生き方についての備忘録。

【Rails】パンくずリストを作るbreadcrumbs_on_railsを使って構造化データ対応する

Ruby on Railsで「パンくずリスト」を簡単に実現してくれるGembreadcrumbs_on_railsを使って「構造化データ」を実現する方法を紹介します。

構造化データとは!?

構造化データとはGoogle公式の情報の記載を引用します。

Google 検索では、ページのコンテンツを理解するよう取り組んでいます。ページに構造化データを含めて、ページの内容についての明白な判断材料を提供すると、Google でそのページをより正確に理解できるようになります。構造化データとは、ページに関する情報を提供し、ページ コンテンツ(たとえばレシピのページでは、材料、加熱時間と加熱温度、カロリーなど)を分類するための標準化されたデータ形式です。

そのままですが、ページ情報を正しくGoogleに伝えるための仕組みです。

すごく多くの種類があるのですが、例えば以下のようなものです。

  • 記事
  • 書籍
  • パンくずリスト
  • カルーセル
  • イベント
  • ライブ配信

今回は、この中の「パンくずリスト」を表示させるためにどうすれば良いか!?をRails使って実現したいと思います。

f:id:opiyotan:20191012032019p:plain
パンくずリストのGoogle表示例

各種類のイメージや使い方はこちらから参照してください。 https://developers.google.com/search/docs/guides/search-gallery?hl=ja

breadcrumbs_on_railsの使い方

Gem breadcrumbs_on_railsの使い方は以下の記事でまとめてありますので、こちらを参照してください。

opiyotan.hatenablog.com

実装方法

パンくずリストの構造化データを実現するには以下のような記載が必要です。

<ol itemscope itemtype="https://schema.org/BreadcrumbList">
  <li itemprop="itemListElement" itemscope
      itemtype="https://schema.org/ListItem">
    <!-- Method 1 (preferred) -->
    <a itemprop="item" href="https://example.com/books">
        <span itemprop="name">Books</span></a>
    <meta itemprop="position" content="1" />
  </li>
<ol>

見慣れないプロパティがありますが、これが構造化データですよ!と伝える為の記述になります。

詳細な内容は、こちらのGoogle公式のページに記載されているのでチェックして見てください。

https://developers.google.com/search/docs/data-types/breadcrumb?hl=ja

今回は、これをrails x gem breadcrumbs_on_railsを使って実現していきます。

Viewの記述

# haml
.breadcrumbs{itemscope: '', itemtype: 'http://schema.org/BreadcrumbList'}
  %ul
    - @breadcrumbs.each.with_index(1) do |breadcrumb, i|
      %li{itemprop: 'itemListElement', itemscope: '', itemtype: 'http://schema.org/ListItem'}
        - if breadcrumb.path.present?
          %a{href: breadcrumb.path, itemprop: 'item'}
            %span{:itemprop => 'name'}= breadcrumb.name
          %meta{:itemprop => 'position', content: i}
        - else
          %span{:itemprop => 'name'}= breadcrumb.name
          %meta{:itemprop => 'position', content: i}

# html
<div class="breadcrumbs" itemscope="" itemtype="http://schema.org/BreadcrumbList">
  <ul>
    <li itemprop="itemListElement" itemscope="" itemtype="http://schema.org/ListItem">
      <a href="root_url" itemprop="item">
          <span itemprop="name">HOME</span>
      </a>
      <meta content="1" itemprop="position">
    </li>
    <li itemprop="itemListElement" itemscope="" itemtype="http://schema.org/ListItem">
      <span itemprop="name">登録ユーザーの一覧</span>
      <meta content="2" itemprop="position">
    </li>
  </ul>
</div>

構造化データが正しく実装できているかは、構造化データ テストツールから確認できます!

構造化データ テストツールのイメージ画像
構造化データ テストツールのイメージ画像

失敗しているとエラーでお知らせしてくれます。

構造化データ テストツールのイメージ画像(失敗例)
構造化データ テストツールのイメージ画像(失敗例)

【Rails】パンくずリストを簡単に作る Gem breadcrumbs_on_rails

Ruby on Railsで「パンくずリスト」を生成するGembreadcrumbs_on_railsの紹介です。

導入方法

Gemfileにbreadcrumbs_on_railsを追加してbundle installします。

# Gemfile
gem 'breadcrumbs_on_rails'

パンくずリストを登録する(Controller)

パンくずリストを表示させたいContollerのアクション内でパンくずリストを登録します。

# new_users_controller.rb
def index
  @users = NewUser.all
  add_breadcrumb 'HOME', :root_url
  add_breadcrumb '登録ユーザーの一覧'
end

使い方はadd_breadcrumb 表示する文字列, パスです。第二引数は省略すると文字列だけ表示されます。

また、階層順になるように設定してください。上記の場合はHOME > 登録ユーザーの一覧の並びになります。

上記の例ではアクションの中で定義していますが、アプリ全体で定義したい場合はApplicationControllerで。コントローラー全体で定義したい場合はアクションの外に定義できます。なので、今回の場合は以下のように記述した方が良いかもしれないです。

class ApplicationController < ActionController::Base
  add_breadcrumb 'HOME', :root_url
end

class NewUsersController < ApplicationController
  add_breadcrumb 'ユーザー一覧', :new_users_path

  def index
  end

  def show
    add_breadcrumb @user.name
  end
end

こうすることで、アクション毎に記述する必要が無くなります。この例だと、HOME > ユーザー一覧 > opiyoみたいな感じで表示されます。

パンくずリストを表示する(View)

パンくずリストの表示方法も簡単で、以下のように記述するだけです。

.breadcrumbs
  %ul
    <li>
    = render_breadcrumbs separator: '</li><li>'
    </li>

パンくずリストの例(css適用前)
パンくずリストの例(css適用前)

ただ、画像のように何も設定しないと縦並びになってしまうのでcssで見た目を整えます。

.breadcrumbs
  %ul
    <li>
    = render_breadcrumbs separator: '</li><li>'
    </li>

:sass
  .breadcrumbs
    text-align: center
    padding: 10px 0
    ul
      li
        display: inline-block
  .breadcrumbs ul > li + li:before
      content: "\f105"
      font-family: FontAwesome
      padding: 0 20px
      display: inline-block

パンくずリストの例(css適用)
パンくずリストの例(css適用)

【Rails】Active Recordで作ったオブジェクトを検索しよう!(find/where)

Ruby on Railsでデータベースを操作するのに使うActive Record。

今回は、findwhereを使って実現するオブジェクトの検索方法を紹介します。

今回利用するテーブル情報はこちらです。 f:id:opiyotan:20191003222737p:plain

基本的な使い方

検索するメソッドはいっぱいあるのですが、よく使う項目は以下のようなのがあります。

  • find
  • find_by
  • find_by!
  • first
  • last
  • where

1件だけ取得したり、条件に応じて複数データ取得できたりします。

これらの違いを一つずつ実行しながら見ていきたいと思います。

条件に合致したデータを1件取得する

これらのメソッドは条件に合致したデータを1件取得するメソッドです。

  • first
  • last
  • find
  • find_by
  • find_by!
  • take

idが一番最初のデータを取得する(first)

> NewUser.first
  NewUser Load (0.4ms)  SELECT  "new_users".* FROM "new_users" ORDER BY "new_users"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<NewUser:0x00007f8f237f3758 id: 1, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, updated_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, token: nil>

idが一番最後のデータを取得する(last)

> NewUser.last
  NewUser Load (0.4ms)  SELECT  "new_users".* FROM "new_users" ORDER BY "new_users"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> #<NewUser:0x00007f8f08033420 id: 10, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 10 Oct 2019 19:26:05 JST +09:00, updated_at: Thu, 10 Oct 2019 19:26:05 JST +09:00, token: "PXFWyWaE5KfYUtjMxE8DNSDF">

指定したidのデータを取得する(find)

> NewUser.find 1
  NewUser Load (0.5ms)  SELECT  "new_users".* FROM "new_users" WHERE "new_users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
=> #<NewUser:0x00007f8f2478bd28 id: 1, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, updated_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, token: nil>

条件に合致したデータを取得する(find_by)

# 仮に複数件マッチした場合でも1件のみ取得します
> NewUser.find_by(name: 'opiyo')
  NewUser Load (0.4ms)  SELECT  "new_users".* FROM "new_users" WHERE "new_users"."name" = $1 LIMIT $2  [["name", "opiyo"], ["LIMIT", 1]]
=> #<NewUser:0x00007f8f09483498 id: 1, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, updated_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, token: nil>
> NewUser.find_by(name: 'opiyoopiyoopiyo')
  NewUser Load (0.4ms)  SELECT  "new_users".* FROM "new_users" WHERE "new_users"."name" = $1 LIMIT $2  [["name", "opiyoopiyoopiyo"], ["LIMIT", 1]]
=> nil

find_byと同じだが取得できない場合は例外が発生(find_by!)

> NewUser.find_by!(name: 'opiyoopiyoopiyo')
  NewUser Load (0.5ms)  SELECT  "new_users".* FROM "new_users" WHERE "new_users"."name" = $1 LIMIT $2  [["name", "opiyoopiyoopiyo"], ["LIMIT", 1]]
ActiveRecord::RecordNotFound: Couldn't find NewUser
from /Users/tnakano/rails/iemiru/vendor/bundle/ruby/2.5.0/gems/activerecord-5.2.3/lib/active_record/core.rb:217:in `find_by!'

条件に合致したデータを複数取得する(where)

whereを使うと条件に応じたデータを複数件取得することができます。

> NewUser.where(name: 'opiyo')
  NewUser Load (0.5ms)  SELECT "new_users".* FROM "new_users" WHERE "new_users"."name" = $1  [["name", "opiyo"]]
=> [#<NewUser:0x00007f8f26d165e8 id: 1, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, updated_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f26d16458 id: 4, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, updated_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f26d162c8 id: 7, name: "opiyo", age: 2, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:29:20 JST +09:00, updated_at: Mon, 07 Oct 2019 22:32:12 JST +09:00, token: nil>

> NewUser.where(name: 'opiyo', age: 2)
  NewUser Load (2.0ms)  SELECT "new_users".* FROM "new_users" WHERE "new_users"."name" = $1 AND "new_users"."age" = $2  [["name", "opiyo"], ["age", 2]]
=> [#<NewUser:0x00007f8f093894e8 id: 7, name: "opiyo", age: 2, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:29:20 JST +09:00, updated_at: Mon, 07 Oct 2019 22:32:12 JST +09:00, token: nil>]

また、条件関係なく全データ取得したい場合は、allを使います。

> NewUser.all
  NewUser Load (0.5ms)  SELECT "new_users".* FROM "new_users"
=> [#<NewUser:0x00007f8f26f57fa0 id: 1, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, updated_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f26f57e10 id: 2, name: nil, age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:41:35 JST +09:00, updated_at: Mon, 07 Oct 2019 21:41:35 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f26f57c80 id: 3, name: nil, age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:42:31 JST +09:00, updated_at: Mon, 07 Oct 2019 21:42:31 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f26f57af0 id: 4, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, updated_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f26f57960 id: 5, name: nil, age: 1, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:22:18 JST +09:00, updated_at: Mon, 07 Oct 2019 22:22:18 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f26f577d0 id: 6, name: nil, age: 2, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:23:25 JST +09:00, updated_at: Mon, 07 Oct 2019 22:23:25 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f26f57640 id: 7, name: "opiyo", age: 2, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:29:20 JST +09:00, updated_at: Mon, 07 Oct 2019 22:32:12 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f26f574b0 id: 8, name: "not_token", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 10 Oct 2019 19:25:07 JST +09:00, updated_at: Thu, 10 Oct 2019 19:25:07 JST +09:00, token: nil>]

whereで取得した以外のデータを取得する(where.not)

> NewUser.where.not(name: 'opiyo')
  NewUser Load (0.4ms)  SELECT "new_users".* FROM "new_users" WHERE "new_users"."name" != $1  [["name", "opiyo"]]
=> [#<NewUser:0x00007f8f08858268 id: 8, name: "not_token", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 10 Oct 2019 19:25:07 JST +09:00, updated_at: Thu, 10 Oct 2019 19:25:07 JST +09:00, token: nil>]

グルーピング(group)

# 人毎の年齢合計
> NewUser.group(:name).sum(:age)
   (2.5ms)  SELECT SUM("new_users"."age") AS sum_age, "new_users"."name" AS new_users_name FROM "new_users" GROUP BY "new_users"."name"
=> {nil=>3, "opiyo"=>2, "not_token"=>0}

テーブルの結合(joins)

> users = NewUser.select('new_users.name, posts.title').joins(:posts).where("posts.title = '1'")
  NewUser Load (0.6ms)  SELECT new_users.name, posts.title FROM "new_users" INNER JOIN "posts" ON "posts"."new_user_id" = "new_users"."id" WHERE (posts.title = '1')
=> [#<NewUser:0x00007f8f09b63e20 id: nil, name: "opiyo">]

> users.each do |user|
*   puts user.name  
*   puts user.title
* end  
opiyo
1

件数を取得する(count)

> NewUser.where(name: 'opiyo').count
   (0.5ms)  SELECT COUNT(*) FROM "new_users" WHERE "new_users"."name" = $1  [["name", "opiyo"]]
=> 3

重複を削除(distinct)

> NewUser.select(:name).distinct
  NewUser Load (0.4ms)  SELECT DISTINCT "new_users"."name" FROM "new_users"
=> [#<NewUser:0x00007f8f1e892ee8 id: nil, name: nil>, #<NewUser:0x00007f8f1e88a108 id: nil, name: "opiyo">, #<NewUser:0x00007f8f1e870c08 id: nil, name: "not_token">]

並び順の指定(order)

# 昇順
> NewUser.where(name: 'opiyo').order(:id)
  NewUser Load (0.5ms)  SELECT "new_users".* FROM "new_users" WHERE "new_users"."name" = $1 ORDER BY "new_users"."id" ASC  [["name", "opiyo"]]
=> [#<NewUser:0x00007f8f0844a6d0 id: 1, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, updated_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f0844a540 id: 4, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, updated_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f0844a3b0 id: 7, name: "opiyo", age: 2, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:29:20 JST +09:00, updated_at: Mon, 07 Oct 2019 22:32:12 JST +09:00, token: nil>]

# 降順
> NewUser.where(name: 'opiyo').order(id: :desc)
  NewUser Load (0.4ms)  SELECT "new_users".* FROM "new_users" WHERE "new_users"."name" = $1 ORDER BY "new_users"."id" DESC  [["name", "opiyo"]]
=> [#<NewUser:0x00007f8f092892a0 id: 7, name: "opiyo", age: 2, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:29:20 JST +09:00, updated_at: Mon, 07 Oct 2019 22:32:12 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f09289110 id: 4, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, updated_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f09288f80 id: 1, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, updated_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, token: nil>]

# 複数指定する場合は文字列で囲み、カンマ区切りで複数記述します
> NewUser.where(name: 'opiyo').order('id desc, created_at')
  NewUser Load (0.5ms)  SELECT "new_users".* FROM "new_users" WHERE "new_users"."name" = $1 ORDER BY id desc, created_at  [["name", "opiyo"]]
=> [#<NewUser:0x00007f8f09032290 id: 7, name: "opiyo", age: 2, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:29:20 JST +09:00, updated_at: Mon, 07 Oct 2019 22:32:12 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f09032100 id: 4, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, updated_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, token: nil>,
 #<NewUser:0x00007f8f09031f70 id: 1, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, updated_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, token: nil>]

【Rails】PostgreSQLをローカル環境(Mac)にインストールする

Ruby on Railsでよく使われるPostgreSQLをローカル(Mac)環境にインストールする方法です。

PostgreSQLのインストール

$ brew install postgresql

PostgreSQLのバージョンチェック

$ psql --version
psql (PostgreSQL) 9.6.2

PostgreSQLサーバの起動

$ postgres -D /usr/local/var/postgres

or

$ brew services start postgresql
 # なんか色々な方法があるみたいです。こっちの方が直感的でわかりやすいですね。

$ brew services list # 起動状況の確認ができる。初めて実行した時だけ`homebrew/services`がインストールされた。

==> Tapping homebrew/services
Cloning into '/usr/local/Homebrew/Library/Taps/homebrew/homebrew-services'...
remote: Counting objects: 10, done.
remote: Compressing objects: 100% (7/7), done.
remote: Total 10 (delta 0), reused 5 (delta 0), pack-reused 0
Unpacking objects: 100% (10/10), done.
Tapped 0 formulae (37 files, 50.7KB)
Name       Status  User Plist
postgresql started taku /Users/taku/Library/LaunchAgents/homebrew.mxcl.postgresql.plist

http://qiita.com/takuya0301/items/c0720753de98572703b8

PostgreSQL のデータベース一覧を表示する

$ psql -l

ロールの登録

$ psql postgres 
postgres=# ¥du #登録されているロール情報の表示
postgres=# create role projectname with createdb login password 'password'; # createdbができる権限の持ったロールを作成する
postgres=# ¥q #終了

PostgreSQL のデータベースを登録する

$ createdb hoge

PostgreSQL のデータベースを削除する

$ dropdb hoge

その他

Railsを動かす場合は、Railsプロジェクトのconfig/database.ymlに記載されている設定に合わせてロールとDBを作る

development:
  adapter: postgresql
  encoding: unicode
  database: project_database
  username: projectname
  host: 127.0.0.1

【Rails】ActiveRecordじゃなくて生のSQLを実行する方法(find_by_sql)

Ruby on Railsで生のSQLを実行する方法です。

ちょっと複雑なデータを取得したい時に、Active Recordだと逆に面倒だったりして生のSQLを描きたい場合があります。

その場合はfind_by_sqlを使います。

基本的な使い方

使い方はシンプルでModel.find_by_sql(sql)です。

> sql = 'select * from new_users;'
=> "select * from new_users;"

> NewUser.find_by_sql(sql)
  NewUser Load (5.3ms)  select * from new_users;
=> [#<NewUser:0x00007fd740f61e30 id: 1, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, updated_at: Mon, 07 Oct 2019 21:09:45 JST +09:00>,
 #<NewUser:0x00007fd73d2380b8 id: 2, name: nil, age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:41:35 JST +09:00, updated_at: Mon, 07 Oct 2019 21:41:35 JST +09:00>,
 #<NewUser:0x00007fd741ee7f08 id: 3, name: nil, age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:42:31 JST +09:00, updated_at: Mon, 07 Oct 2019 21:42:31 JST +09:00>,
 #<NewUser:0x00007fd741ee7d78 id: 4, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, updated_at: Mon, 07 Oct 2019 22:03:40 JST +09:00>,
 #<NewUser:0x00007fd741ee7be8 id: 5, name: nil, age: 1, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:22:18 JST +09:00, updated_at: Mon, 07 Oct 2019 22:22:18 JST +09:00>,
 #<NewUser:0x00007fd741ee7a58 id: 6, name: nil, age: 2, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:23:25 JST +09:00, updated_at: Mon, 07 Oct 2019 22:23:25 JST +09:00>,
 #<NewUser:0x00007fd741ee78c8 id: 7, name: "opiyo", age: 2, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:29:20 JST +09:00, updated_at: Mon, 07 Oct 2019 22:32:12 JST +09:00>]

動的なパラメーターを渡す

whereなどに動的に値を渡したい場合は、値を設定したい場所を?にし、配列で渡します。

> sql = 'select * from new_users where name = ?;'
=> "select * from new_users where name = ?;"

> NewUser.find_by_sql([sql, 'opiyo'])
  NewUser Load (0.5ms)  select * from new_users where name = 'opiyo';
=> [#<NewUser:0x00007fd742cf1798 id: 1, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, updated_at: Mon, 07 Oct 2019 21:09:45 JST +09:00>,
 #<NewUser:0x00007fd742cf1608 id: 4, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, updated_at: Mon, 07 Oct 2019 22:03:40 JST +09:00>,
 #<NewUser:0x00007fd742cf1478 id: 7, name: "opiyo", age: 2, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:29:20 JST +09:00, updated_at: Mon, 07 Oct 2019 22:32:12 JST +09:00>]

また、シンボル:nameを使ってハッシュ値で渡すことも可能です。

> sql = 'select * from new_users where name = :name;'
=> "select * from new_users where name = :name;"

> NewUser.find_by_sql([sql, { name: 'opiyo' }])
  NewUser Load (1.9ms)  select * from new_users where name = 'opiyo';
=> [#<NewUser:0x00007fd726052c88 id: 1, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, updated_at: Mon, 07 Oct 2019 21:09:45 JST +09:00>,
 #<NewUser:0x00007fd726052af8 id: 4, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, updated_at: Mon, 07 Oct 2019 22:03:40 JST +09:00>,
 #<NewUser:0x00007fd726052968 id: 7, name: "opiyo", age: 2, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:29:20 JST +09:00, updated_at: Mon, 07 Oct 2019 22:32:12 JST +09:00>]

複数ある場合も同様に?やシンボルを使えばokです。

> name = 'opiyo'
=> "opiyo"

> from = Date.current.beginning_of_month
=> Tue, 01 Oct 2019

> to = Date.current.end_of_month
=> Thu, 31 Oct 2019

> sql = "select * from new_users where name = :name and created_at between :from AND :to;"
=> "select * from new_users where name = :name and created_at between :from AND :to;"
[88] pry(main)> NewUser.find_by_sql([sql, { name: name, from: from.to_s, to: to.to_s }])
  NewUser Load (0.5ms)  select * from new_users where name = 'opiyo' and created_at between '2019/10/01' AND '2019/10/31';
=> [#<NewUser:0x00007fd741af0c88 id: 1, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 21:09:45 JST +09:00, updated_at: Mon, 07 Oct 2019 21:09:45 JST +09:00>,
 #<NewUser:0x00007fd741af0878 id: 4, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:03:40 JST +09:00, updated_at: Mon, 07 Oct 2019 22:03:40 JST +09:00>,
 #<NewUser:0x00007fd741af05a8 id: 7, name: "opiyo", age: 2, gender: nil, birthday: nil, email: nil, created_at: Mon, 07 Oct 2019 22:29:20 JST +09:00, updated_at: Mon, 07 Oct 2019 22:32:12 JST +09:00>]

テーブルを結合するような場合も普通に出来ます。

> sql
=> "select new_users.name, posts.title from new_users join posts on new_users.id = posts.id"

> new_user_posts = NewUser.find_by_sql(sql)
  NewUser Load (0.5ms)  select new_users.name, posts.title from new_users join posts on new_users.id = posts.id
=> [#<NewUser:0x00007fd7421a2bf8 id: nil, name: "opiyo">,
 #<NewUser:0x00007fd7421a29a0 id: nil, name: nil>,
 #<NewUser:0x00007fd7421a2658 id: nil, name: nil>,
 #<NewUser:0x00007fd7421a22e8 id: nil, name: "opiyo">,
 #<NewUser:0x00007fd7421a20e0 id: nil, name: nil>]

> new_user_posts.first
=> #<NewUser:0x00007fd7421a2bf8 id: nil, name: "opiyo">

> new_user_posts.first.attributes
=> {"id"=>nil, "name"=>"opiyo", "title"=>"一番目"}

> new_user_posts.first.name
=> "opiyo"

> new_user_posts.first.title
=> "一番目"

select指定するとパッと見取得できないような感じですが、attributesで確認するとバッチリ取得できてます。

あとは、いつもと同じようにuser.nameとか使えるのでいかように!

【Rails保存版】Active Recordで作ったオブジェクトを保存しよう!(save/update)

Ruby on Railsでデータベースを操作するのに使うActive Record。

今回は、saveupdateを使って実現するオブジェクトの保存方法紹介します。

今回利用するテーブル情報はこちらです。 f:id:opiyotan:20191003222737p:plain

基本的な使い方

保存する役割を果たすメソッドは以下の通りいっぱいあります。

  • save
  • save!
  • update
  • update!
  • update_attribute
  • update_attributes
  • update_attributes!
  • update_column
  • update_columns
  • update_all

それぞれの違いは、「戻り値の違い」と「コールバックの有無」です。

使い所や実行結果などをそれぞれ見ていきたいと思います。

save

  • よく使う処理: コントローラーのcreate
  • 戻り値: true/false

基本的に利用するのは、createアクションです。戻り値がtrue/falseになるので保存が成功したか失敗したかで処理を分けるように記述します。

# new_users_controller.rb
def create
  @user = NewUser.new(new_user_params)

  if @user.save
    redirect_to :new_users_path, notice: 'ユーザーを登録しました。'
  else
    render :new
  end
end
# 成功する場合
> user.name = 'opiyo'
=> "opiyo"
> user.save
   (0.3ms)  BEGIN
  NewUser Create (0.9ms)  INSERT INTO "new_users" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "opiyo"], ["created_at", "2019-10-07 12:09:45.517006"], ["updated_at", "2019-10-07 12:09:45.517006"]]
   (0.6ms)  COMMIT
=> true

# 失敗する場合
> user.save
   (0.3ms)  BEGIN
   (4.8ms)  ROLLBACK
=> false

save!

  • よく使う処理: モデル、rakeタスク
  • 戻り値: 例外(ActiveRecord::RecordInvalid)

基本的に利用するのは、モデルやrakeタスクなどで利用します。合わせて例外処理を書く場面が多いと思います。

self.transaction do
  self.name = 'opiyo'
  self.save!
end

begin
  user.name = 'opiyo'
  user.save!
rescue => e
  puts e
end

update

  • よく使う処理: コントローラーのupdate
  • 戻り値: true/false

基本的に利用するのは、updateアクションです。

def update
  if @user.update new_user_params
    redirect_to new_users_path(@magazine), notice: 'ユーザーを保存しました'
  else
    render 'edit'
  end
end
> user.update(name: 'opiyo')
   (0.3ms)  BEGIN
  NewUser Create (5.8ms)  INSERT INTO "new_users" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "opiyo"], ["created_at", "2019-10-07 13:03:40.364928"], ["updated_at", "2019-10-07 13:03:40.364928"]]
   (1.3ms)  COMMIT
=> true

update!

  • よく使う処理: モデル
  • 戻り値: 例外(ActiveRecord::RecordInvalid)

基本的に利用するのは、モデルやrakeタスクなどで利用します。合わせて例外処理を書く場面が多いと思います。

self.transaction do
  self.save!(name: 'opiyo')
end

begin
  self.save!(name: 'opiyo')
rescue => e
  puts e
end

update_attribute

  • よく使う処理: モデルやrakeタスク
  • 戻り値: true/false

基本的に利用するのは、モデルやrakeタスクです。

updateとの違いは、保存できるのは1つのカラムでvalidationを無視します。

# validationの設定
class NewUser < ApplicationRecord
  validates :name, presence: true
end

# 実行結果
> user.update(age: 1)
   (0.3ms)  BEGIN
   (0.3ms)  ROLLBACK
=> false

> user.update_attribute(:age, 1)
   (0.3ms)  BEGIN
  NewUser Create (0.4ms)  INSERT INTO "new_users" ("age", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["age", 1], ["created_at", "2019-10-07 13:29:20.417777"], ["updated_at", "2019-10-07 13:29:20.417777"]]
   (2.0ms)  COMMIT
=> true

update_attributes

  • よく使う処理: モデルやrakeタスク
  • 戻り値: true/false

基本的に利用するのは、モデルやrakeタスクです。

update_attributeとの違いは、複数保存ができ、validationは実行されます。なので処理の内容としてはupdateと全く同じです。

> user.update_attributes(age: 2)
   (0.3ms)  BEGIN
   (0.2ms)  ROLLBACK
=> false
> user.update_attributes(name: 'opiyo', age: 2)
   (0.3ms)  BEGIN
  NewUser Update (0.6ms)  UPDATE "new_users" SET "name" = $1, "age" = $2, "updated_at" = $3 WHERE "new_users"."id" = $4  [["name", "opiyo"], ["age", 2], ["updated_at", "2019-10-07 13:32:12.006308"], ["id", 7]]
   (2.9ms)  COMMIT
=> true

update_attributes!

  • よく使う処理: モデルやrakeタスク
  • 戻り値: 例外(ActiveRecord::RecordInvalid)

基本的に利用するのは、モデルやrakeタスクです。

update_attributesとの違いは、例外が発生します。なので処理の内容としてはupdate!と全く同じです。

update_column

  • よく使う処理: ajax使ってデータ更新するときやモデルやrakeタスク
  • 戻り値: 例外(ActiveRecord::RecordInvalid)

基本的に利用するのは、モデルやrakeタスクです。

updateupdate_attributeとの違いは、「validationを無視」「コールバック無視」「update_at無視」です。つまり保存するだけ。

update_columns

  • よく使う処理: ajax使ってデータ更新するときやモデルやrakeタスク
  • 戻り値: 例外(ActiveRecord::RecordInvalid)

基本的に利用するのは、モデルやrakeタスクです。

update_attributesと同じで複数カラムを同時に更新するときに使います。内容はupdate_columnと同じで「validationを無視」「コールバック無視」「update_at無視」です。

まとめ

  • create: save!
  • update: update
  • モデルやrakeタスク: save/update_attributes
  • ただ保存したい: update_columns
  • 例外: !

各メソッドの実行結果や、どうゆう時に何を使うかまとめてきました。

改めて見直すと、各メソッドの微妙な違いを意識して処理できてたか曖昧な部分もあるので見直したいなーと思いました。

【Rails】Active Recordで作ったオブジェクトを削除しよう!(destroy/delete)

Ruby on Railsでデータベースを操作するのに使うActive Record。

今回は、destroydeleteを使って実現するオブジェクトの削除方法紹介します。

今回利用するテーブル情報はこちらです。

今回使うテーブルのER図(new_users/posts)
今回使うテーブルのER図

基本的なこと

オブジェクトを削除するメソッドはdestroydeleteです。

# deleteはDELETE文が実行される
> user.posts.last
=> #<Post:0x00007fb4d78bb800 id: 7, user_id: nil, title: "5", body: nil, created_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, updated_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, new_user_id: 1>
> user.posts.last.delete
  Post Destroy (2.2ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 7]]
=> #<Post:0x00007fb4d78bb800 id: 7, user_id: nil, title: "5", body: nil, created_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, updated_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, new_user_id: 1>

# ActiveRecordのdestroyが実行される
> user.posts.last
=> #<Post:0x00007fb4d78bb800 id: 7, user_id: nil, title: "5", body: nil, created_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, updated_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, new_user_id: 1>
> user.posts.last.destroy
   (0.3ms)  BEGIN
   (0.2ms)  COMMIT
=> #<Post:0x00007fb4d78bb800 id: 7, user_id: nil, title: "5", body: nil, created_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, updated_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, new_user_id: 1>

destroy

destroyを実行した場合は関連データを考慮してくれます。

NewUserオブジェクトを削除した時に、紐づくPostオブジェクトも同時に削除してくれます。

# new_user.rb
class NewUser < ApplicationRecord
  has_many :posts, dependent: :destroy
end

これを実現しているのがdependentという設定になるのですが、大きく2種類あります。

  • destory: 関連データを削除
  • nullify: 関連データを削除せずに、nullに更新する

dependent: :destroy

実行結果を見るとわかりますが、先ず始めにPostオブジェクトにDELETE文が走ります。その後にNewUserオブジェクトにDELETE文が実行され関連するデータが全て削除されました。

> Post.all
  Post Load (0.4ms)  SELECT "posts".* FROM "posts"
=> [#<Post:0x00007fed68ebe4e0 id: 14, user_id: nil, title: "1", body: nil, created_at: Thu, 03 Oct 2019 23:20:16 JST +09:00, updated_at: Thu, 03 Oct 2019 23:20:16 JST +09:00, new_user_id: 4>,
 #<Post:0x00007fed68ebe350 id: 15, user_id: nil, title: "2", body: nil, created_at: Thu, 03 Oct 2019 23:20:17 JST +09:00, updated_at: Thu, 03 Oct 2019 23:20:17 JST +09:00, new_user_id: 4>,
 #<Post:0x00007fed68ebe1c0 id: 16, user_id: nil, title: "3", body: nil, created_at: Thu, 03 Oct 2019 23:20:21 JST +09:00, updated_at: Thu, 03 Oct 2019 23:20:21 JST +09:00, new_user_id: 4>]

> NewUser.last.destroy
  NewUser Load (0.5ms)  SELECT  "new_users".* FROM "new_users" ORDER BY "new_users"."id" DESC LIMIT $1  [["LIMIT", 1]]
   (0.2ms)  BEGIN
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."new_user_id" = $1  [["new_user_id", 4]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 14]]
  Post Destroy (0.2ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 15]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 16]]
  NewUser Destroy (0.4ms)  DELETE FROM "new_users" WHERE "new_users"."id" = $1  [["id", 4]]
   (0.5ms)  COMMIT
=> #<NewUser:0x00007fed4b098ab8 id: 4, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:20:02 JST +09:00, updated_at: Thu, 03 Oct 2019 23:20:02 JST +09:00>

> Post.all
  Post Load (0.5ms)  SELECT "posts".* FROM "posts"
=> []

dependent: :nullfy

実行結果を見るとわかりますが、Postオブジェクトは削除されずにnew_user_idがnullにupdateされています。

そのため、Postデータを明示的に残しておきたい場合はdependent: :nullifyを設定します。

> Post.all
  Post Load (0.7ms)  SELECT "posts".* FROM "posts"
=> [#<Post:0x00007fbf14ebfb10 id: 8, user_id: nil, title: "1", body: nil, created_at: Thu, 03 Oct 2019 23:14:38 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:38 JST +09:00, new_user_id: 2>,
 #<Post:0x00007fbf14ebf958 id: 9, user_id: nil, title: "2", body: nil, created_at: Thu, 03 Oct 2019 23:14:40 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:40 JST +09:00, new_user_id: 2>,
 #<Post:0x00007fbf14ebf7c8 id: 10, user_id: nil, title: "3", body: nil, created_at: Thu, 03 Oct 2019 23:14:42 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:42 JST +09:00, new_user_id: 2>]

> NewUser.last.destroy
  NewUser Load (0.4ms)  SELECT  "new_users".* FROM "new_users" ORDER BY "new_users"."id" DESC LIMIT $1  [["LIMIT", 1]]
   (0.2ms)  BEGIN
  Post Update All (0.4ms)  UPDATE "posts" SET "new_user_id" = NULL WHERE "posts"."new_user_id" = $1  [["new_user_id", 2]]
  NewUser Destroy (0.4ms)  DELETE FROM "new_users" WHERE "new_users"."id" = $1  [["id", 2]]
   (1.9ms)  COMMIT
=> #<NewUser:0x00007fbef78b27f8 id: 2, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:14:35 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:35 JST +09:00>

> Post.all
  Post Load (0.4ms)  SELECT "posts".* FROM "posts"
=> [#<Post:0x00007fbef7927b70 id: 8, user_id: nil, title: "1", body: nil, created_at: Thu, 03 Oct 2019 23:14:38 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:38 JST +09:00, new_user_id: nil>,
 #<Post:0x00007fbef79279e0 id: 9, user_id: nil, title: "2", body: nil, created_at: Thu, 03 Oct 2019 23:14:40 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:40 JST +09:00, new_user_id: nil>,
 #<Post:0x00007fbef7927850 id: 10, user_id: nil, title: "3", body: nil, created_at: Thu, 03 Oct 2019 23:14:42 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:42 JST +09:00, new_user_id: nil>]
[12] pry(main)> NewUser.all
  NewUser Load (0.4ms)  SELECT "new_users".* FROM "new_users"
=> []

delete

deleteはイメージ的にSQLのDELETE文を実行するような感じで対象のデータを1件削除します。そのためdestroyよりも処理は速いです。

が、アプリケーションの処理の中ではほとんど使われないのではないでしょうか?

関連データがあった場合にdeleteしちゃうと整合性が取れず壊れちゃうし、関連データがない場合でもdestroyで削除することは可能なので。

全件削除する(destroy_all/delete_all)

1件だけじゃなくて全てのデータを削除したい場合はdestroy_alldelete_allがあります。

# destroy_allの実行例
> NewUser.all
  NewUser Load (0.5ms)  SELECT "new_users".* FROM "new_users"
=> [#<NewUser:0x00007fed66dc46b8 id: 5, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:25:58 JST +09:00, updated_at: Thu, 03 Oct 2019 23:25:58 JST +09:00>,
 #<NewUser:0x00007fed66dc4528 id: 6, name: "fuuzo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:26:26 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:26 JST +09:00>]
> Post.all
  Post Load (0.4ms)  SELECT "posts".* FROM "posts"
=> [#<Post:0x00007fed68bac798 id: 17, user_id: nil, title: "1", body: nil, created_at: Thu, 03 Oct 2019 23:26:03 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:03 JST +09:00, new_user_id: 5>,
 #<Post:0x00007fed68bac608 id: 18, user_id: nil, title: "2", body: nil, created_at: Thu, 03 Oct 2019 23:26:04 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:04 JST +09:00, new_user_id: 5>,
 #<Post:0x00007fed68bac478 id: 19, user_id: nil, title: "3", body: nil, created_at: Thu, 03 Oct 2019 23:26:10 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:10 JST +09:00, new_user_id: 5>,
 #<Post:0x00007fed68bac2e8 id: 20, user_id: nil, title: "11", body: nil, created_at: Thu, 03 Oct 2019 23:26:32 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:32 JST +09:00, new_user_id: 6>,
 #<Post:0x00007fed68bac158 id: 21, user_id: nil, title: "12", body: nil, created_at: Thu, 03 Oct 2019 23:26:34 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:34 JST +09:00, new_user_id: 6>,
 #<Post:0x00007fed68ba7f68 id: 22, user_id: nil, title: "13", body: nil, created_at: Thu, 03 Oct 2019 23:26:35 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:35 JST +09:00, new_user_id: 6>]
> NewUser.destroy_all
  NewUser Load (0.4ms)  SELECT "new_users".* FROM "new_users"
   (0.2ms)  BEGIN
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."new_user_id" = $1  [["new_user_id", 5]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 17]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 18]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 19]]
  NewUser Destroy (0.4ms)  DELETE FROM "new_users" WHERE "new_users"."id" = $1  [["id", 5]]
   (1.7ms)  COMMIT
   (0.2ms)  BEGIN
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."new_user_id" = $1  [["new_user_id", 6]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 20]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 21]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 22]]
  NewUser Destroy (0.4ms)  DELETE FROM "new_users" WHERE "new_users"."id" = $1  [["id", 6]]
   (0.4ms)  COMMIT
=> [#<NewUser:0x00007fed66d75cc0 id: 5, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:25:58 JST +09:00, updated_at: Thu, 03 Oct 2019 23:25:58 JST +09:00>,
 #<NewUser:0x00007fed66d75b30 id: 6, name: "fuuzo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:26:26 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:26 JST +09:00>]
> NewUser.all
  NewUser Load (0.4ms)  SELECT "new_users".* FROM "new_users"
=> []
> Post.all
  Post Load (0.4ms)  SELECT "posts".* FROM "posts"
=> []

実践的に使う

destroy / destroy!

savesave!と同じように!を付けると例外を発生させることが可能です。

基本的には全てdestroy!で問題ないと思います。削除できなかったってことは何か問題が発生したってことなので例外を発生させて気付けるようにしたいので。

条件に応じて削除しないようにする

とはいえ、条件に応じて処理は続行させたい場合もあると思います。

今回の例の場合だと、1件でもPostデータがあればUserデータは削除させないようにするにはbefore_destroyを使う。

# post.rb
class Post < ApplicationRecord
  belongs_to :new_user

  before_destroy :do_not_destroy_posted

  private

  def do_not_destroy_posted
    throw :abort
  end
end
> user = NewUser.last
  NewUser Load (0.7ms)  SELECT  "new_users".* FROM "new_users" ORDER BY "new_users"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> #<NewUser:0x00007fab7fe19150 id: 7, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:43:42 JST +09:00, updated_at: Thu, 03 Oct 2019 23:43:42 JST +09:00>
> user.destroy
   (0.3ms)  BEGIN
  Post Load (0.8ms)  SELECT "posts".* FROM "posts" WHERE "posts"."new_user_id" = $1  [["new_user_id", 7]]
   (0.8ms)  ROLLBACK
=> false

まとめ

削除処理はデータをぶっ壊す可能性がありますので、ちゃんと考えて実装する必要があると思います。

しっかりと関連データまで意識した処理を書いていきたいですね。

  • 基本的にはdestroy!
  • dependentで関連データを扱いを定義する
  • 条件がある場合はbefore_destroyを使ってバリデーションをかける
  • なーんも関係なく削除したい場合はdeleteの方が早い

【Rails】Active Recordでオブジェクトを作ろう!(new/build)

Ruby on Railsでデータベースを操作するのに使うActive Record

今回は、newbuildを使って実現するオブジェクトの作り方を紹介します。

基本的なこと

newbuildは基本的に出来ることは一緒です。

厳密には違うのかもしれませんが、「new build 違い」でググってもあまりヒットしませんでした。(知っている方、ぜひ教えて下さい!)

ですが、使う場面が違ったりするのでそちらを以下で紹介できればと思います。

今回利用するテーブルはこちらです。

# users
 Table "public.users"
         Column         |            Type             | Collation | Nullable |              Default              
------------------------+-----------------------------+-----------+----------+-----------------------------------
 id                     | integer                     |           | not null | nextval('users_id_seq'::regclass)
 email                  | character varying           |           | not null | ''::character varying
 name                   | character varying           |           |          | 
 tel                    | character varying           |           |          | 
 postal_code            | character varying           |           |          | 
 address                | character varying           |           |          | 
 nickname               | character varying           |           |          | 
 gender                 | character varying           |           |          | 
 birthday               | date                        |           |          | 
# posts
                                        Table "public.posts"
   Column   |            Type             | Collation | Nullable |              Default              
------------+-----------------------------+-----------+----------+-----------------------------------
 id         | bigint                      |           | not null | nextval('posts_id_seq'::regclass)
 user_id    | bigint                      |           |          | 
 title      | character varying           |           |          | 
 body       | text                        |           |          | 
 created_at | timestamp without time zone |           | not null | 
 updated_at | timestamp without time zone |           | not null | 
Indexes:
    "posts_pkey" PRIMARY KEY, btree (id)
    "index_posts_on_user_id" btree (user_id)
Foreign-key constraints:
    "fk_rails_5b5ddfd518" FOREIGN KEY (user_id) REFERENCES users(id)
# user_condition
                                          Table "public.user_conditions"
    Column     |            Type             | Collation | Nullable |                   Default                   
---------------+-----------------------------+-----------+----------+---------------------------------------------
 id            | bigint                      |           | not null | nextval('user_conditions_id_seq'::regclass)
 user_id       | bigint                      |           |          | 
 sign_in_count | integer                     |           | not null | 0
 created_at    | timestamp without time zone |           | not null | 
 updated_at    | timestamp without time zone |           | not null | 
Indexes:
    "user_conditions_pkey" PRIMARY KEY, btree (id)
    "index_user_conditions_on_user_id" btree (user_id)
Foreign-key constraints:
    "fk_rails_47c01ca983" FOREIGN KEY (user_id) REFERENCES users(id)

newを使う時

newを使う時は、そのモデル単体で作成したい場合に使います。

Model.newだけ実行すると空っぽの状態で、引数にカラムと設定したい値を付けると値が反映された状態で作成されます。

# 引数なし
> user = User.new
=> #<User id: nil, email: "", created_at: nil, updated_at: nil, name: nil, phonetic: nil, tel: nil, postal_code: nil, address: nil, nickname: nil, gender: nil, birthday: nil>

# 引数あり
> User.new(name: 'opiyo', email: 'kosmo.waizu0804@gmail.com')
=> #<User id: nil, email: "kosmo.waizu0804@gmail.com", created_at: nil, updated_at: nil, name: "opiyo", .....>

実際の処理で使う場面としては、newcreateアクションで使うと思います。

class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new user_params
    if @user.save
      redirect_to users_path
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :postal_code, :address, :email, :tel)
  end
end

buildを使う時

buildを使う時は、1 : Nの関係がある時にN側のモデルを作成したい場合に使います。

# user.rb(1)
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end
# post.rb(N)
class Post < ApplicationRecord
  belongs_to :user
end

newと同じで引数がないと空っぽの状態で、引数があるとその値が反映された状態で作成されます。

# 引数なし
> user = User.last
=> #<User id: 1, email: "kosmo.waizu0804@gmail.com", created_at: "2019-04-04 09:26:02", updated_at: "2019-04-16 12:08:08", name: "opiyo"

> user.posts.build
=> #<Post:0x00007faeee38dfe8 id: nil, user_id: 200, title: nil, body: nil, created_at: nil, updated_at: nil>

# newでも同じことが出来ます
> user.posts.new
=> #<Post:0x00007faeef94e350 id: nil, user_id: 200, title: nil, body: nil, created_at: nil, updated_at: nil>

実際の処理で使う場面としては、newcreateアクションで使うのは同じですがログインが前提の場合や親になるモデルが取得できているような場面で利用します。

class ApplicationController < ActionController::Base
  def current_user
    @current_user = User.find_by(id: session[:user_id])
  end
end

class PostsController < ApplicationController
  def new
    @post = @current_user.posts.build
  end

  def create
    @post = @current_user.posts.build post_params
    if @post.save
      redirect_to users_posts_path
    else
      render :new
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :body)
  end
end

Post.newしてpost.user_id = @current_user.idみたいに書くことも可能ですが、上記のように書くことで最初からuser_idが設定された状態でオブジェクトが作られますのでスマートに書けます。

new/build以外の作り方

1 : 1の関係でオブジェクトを作りたい時はbuild_テーブル名というメソッドを使います。

# user.rb(1)
class User < ApplicationRecord
  has_one :user_condition, dependent: :destroy
end
# user_condition.rb(1)
class UserCondition < ApplicationRecord
  belongs_to :user
end
> user = User.last
=> #<User id: 200, email: "kosmo.waizu0804@gmail.com", created_at: "2019-04-04 09:26:02", updated_at: "2019-04-16 12:08:08", name: "opiyo"...>

> user.user_condition
  UserCondition Load (0.5ms)  SELECT  "user_conditions".* FROM "user_conditions" WHERE "user_conditions"."user_id" = $1 LIMIT $2  [["user_id", 200], ["LIMIT", 1]]
=> nil

# newだとエラー
> user.user_condition.new
NoMethodError: undefined method `new' for nil:NilClass
from (pry):6:in `<main>'

# buildだとエラー
> user.user_condition.build
NoMethodError: undefined method `build' for nil:NilClass
from (pry):7:in `<main>'

> user.build_user_condition
=> #<UserCondition:0x00007feedc2c26d0 id: nil, user_id: 200, sign_in_count: 0, created_at: nil, updated_at: nil>

まとめ

最後に改めてざっとまとめてみると、こんな感じでしょうか。

  • 単体でオブジェクトを作る時はModel.new
  • 1 : Nの関係の時はparent.childs.build
  • 1 : 1の関係の時はmodel.build_テーブル名

では。

【Rails】オブジェクトを作成したり検索したり更新したり保存したり

Ruby on Railsではデータベースとの様々なやりとりをActice Recordという仕組みを使って行いますが、よく使われる方法をざっとご紹介です。

今回利用するテーブル情報はこちらです。

Table "public.hoges"
   Column   |            Type             | Collation | Nullable |              Default              
------------+-----------------------------+-----------+----------+-----------------------------------
 id         | bigint                      |           | not null | nextval('hoges_id_seq'::regclass)
 name       | character varying           |           |          | 
 age        | integer                     |           |          | 
 gender     | character varying           |           |          | 
 birthday   | date                        |           |          | 
 email      | character varying           |           |          | 
 created_at | timestamp without time zone |           | not null | 
 updated_at | timestamp without time zone |           | not null | 
Indexes:
    "hoges_pkey" PRIMARY KEY, btree (id)

オブジェクトを作成する

オブジェクトを作成するときは、newbuildを使います。

> Hoge.new
=> #<Hoge:0x00007fe9b5bbce10 id: nil, name: nil, age: nil, gender: nil, birthday: nil, email: nil, created_at: nil, updated_at: nil>
# hogeに紐づく記事データを作成する(1 : Nの関係)
def new
  hoge = Hoge.find params[:id]
  @hoge = hoge.articles.build(strong_parameter)
end

# hogeに紐づく家族構成データを作成する(1 : 1の関係)
def new
  hoge = Hoge.find params[:id]
  @hoge = hoge.build_family_structure(strong_parameter)
end

newは単体のモデルを生成する時、buildはテーブルの関係が1 : N(has_many)の場面で使います。

ログインが前提になるようなサービスだとnewを使う場面は、あんまり無いと思います。

build_モデル名はテーブルの関係が1 : 1(has_one)の場面で利用します。

【20191002追記】

より詳細な情報・書き方まとめてみました!

opiyotan.hatenablog.com

オブジェクトを検索する

オブジェクトを検索する = SQLで言うwhere文を実行する処理です。

  • find
  • find_by
  • find_by!
  • first
  • last
  • where
# idが1のものを1件取得
> Hoge.find 1
  Hoge Load (0.5ms)  SELECT  "hoges".* FROM "hoges" WHERE "hoges"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
=> #<Hoge:0x00007fe9b645f4a0 id: 1, name: "opiyo", age: 30, gender: nil, birthday: Fri, 04 Aug 1989, email: nil, created_at: Mon, 30 Sep 2019 20:47:43 JST +09:00, updated_at: Mon, 30 Sep 2019 20:47:43 JST +09:00>

# nameが'opiyo'のものを1件取得
> Hoge.find_by(name: 'opiyo')
  Hoge Load (0.4ms)  SELECT  "hoges".* FROM "hoges" WHERE "hoges"."name" = $1 LIMIT $2  [["name", "opiyo"], ["LIMIT", 1]]
=> #<Hoge:0x00007fe9d4d21e30 id: 1, name: "opiyo", age: 30, gender: nil, birthday: Fri, 04 Aug 1989, email: nil, created_at: Mon, 30 Sep 2019 20:47:43 JST +09:00, updated_at: Mon, 30 Sep 2019 20:47:43 JST +09:00>

# 一番最初に登録されたものを取得
> Hoge.first
  Hoge Load (0.3ms)  SELECT  "hoges".* FROM "hoges" ORDER BY "hoges"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<Hoge:0x00007fe9b6242e60 id: 1, name: "opiyo", age: 30, gender: nil, birthday: Fri, 04 Aug 1989, email: nil, created_at: Mon, 30 Sep 2019 20:47:43 JST +09:00, updated_at: Mon, 30 Sep 2019 20:47:43 JST +09:00>

# 一番最後に登録されたものを取得
> Hoge.last
  Hoge Load (0.4ms)  SELECT  "hoges".* FROM "hoges" ORDER BY "hoges"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> #<Hoge:0x00007fe9b61b97c8 id: 2, name: "tako", age: 20, gender: nil, birthday: Sat, 23 Oct 1999, email: nil, created_at: Tue, 01 Oct 2019 21:50:26 JST +09:00, updated_at: Tue, 01 Oct 2019 21:50:26 JST +09:00>

# 条件に一致するものを複数件取得
> Hoge.where(gender: nil)
  Hoge Load (0.4ms)  SELECT "hoges".* FROM "hoges" WHERE "hoges"."gender" IS NULL
=> [#<Hoge:0x00007fe9b59b6ff8 id: 1, name: "opiyo", age: 30, gender: nil, birthday: Fri, 04 Aug 1989, email: nil, created_at: Mon, 30 Sep 2019 20:47:43 JST +09:00, updated_at: Mon, 30 Sep 2019 20:47:43 JST +09:00>,
 #<Hoge:0x00007fe9b59b6e40 id: 2, name: "tako", age: 20, gender: nil, birthday: Sat, 23 Oct 1999, email: nil, created_at: Tue, 01 Oct 2019 21:50:26 JST +09:00, updated_at: Tue, 01 Oct 2019 21:50:26 JST +09:00>]

# データが見つからなかった時
> Hoge.find_by!(name: 'opi')
  Hoge Load (0.4ms)  SELECT  "hoges".* FROM "hoges" WHERE "hoges"."name" = $1 LIMIT $2  [["name", "opi"], ["LIMIT", 1]]
ActiveRecord::RecordNotFound: Couldn't find Hoge

少しややこしいのが、find_bywhereです。find_byは必ず1件のデータのみ返却しますので1件を保証される場合はこちらを使います。

また、find_by!にすることで1件も見つからない場合は例外を発生させることもあります。

joingrouporderなども出来ますので、こちらはまた別の記事でまとめてみたいと思います。

【20191007追記】

より詳細な情報・書き方まとめてみました!

opiyotan.hatenablog.com

オブジェクトの保存・更新

オブジェクトの保存・更新は種類がいっぱいあります。

  • save
  • save!
  • update
  • update!
  • update_attribute
  • update_attributes
  • update_attributes!
  • update_column
  • update_columns
  • update_all

全てデータの保存や更新する処理になりますが、違いのポイントとしては以下があります。

  • 戻り値の違い「true/false」 or 「例外」
  • コールバックアクション(before_action/after_action)の有無
  • バリデーションの有無

!がついた場合は例外を返します。

update_columnはコールバックやバリデーションを無視しますので、ajaxやrakeタスクなど非同期で処理している時に使うことが多いと思います。が、基本的にはupdate_attributesを使います。

【20191007追記】

より詳細な情報・書き方まとめてみました!

opiyotan.hatenablog.com

オブジェクトの削除

削除する処理はdestroydeleteの二つになります。

  • destroy
  • delete
  • destroy_all
  • delete_all

大きな違いは、destroyの場合は関連するデータも削除しますがdeleteの場合はそのデータのみを削除します。

【20191003追記】

より詳細な情報・書き方まとめてみました!

opiyotan.hatenablog.com

合わせて一本!

探して作って、探して保存してを実現する便利なメソッドもあるのでそれもご紹介です!

  • find_or_created_by!
  • find_or_initialize_by
# findして、あればデータを無ければnew
> Hoge.find_or_initialize_by(name: 'opiyo')
  Hoge Load (0.5ms)  SELECT  "hoges".* FROM "hoges" WHERE "hoges"."name" = $1 LIMIT $2  [["name", "opiyo"], ["LIMIT", 1]]
=> #<Hoge:0x00007fe9d4d57418 id: 1, name: "opiyo", age: 30, gender: nil, birthday: Fri, 04 Aug 1989, email: nil, created_at: Mon, 30 Sep 2019 20:47:43 JST +09:00, updated_at: Mon, 30 Sep 2019 20:47:43 JST +09:00>

> Hoge.find_or_initialize_by(name: 'opiy')
  Hoge Load (0.5ms)  SELECT  "hoges".* FROM "hoges" WHERE "hoges"."name" = $1 LIMIT $2  [["name", "opiy"], ["LIMIT", 1]]
=> #<Hoge:0x00007fe9d4c692b8 id: nil, name: "opiy", age: nil, gender: nil, birthday: nil, email: nil, created_at: nil, updated_at: nil>

# findして、あればデータを無ければcreate
> Hoge.find_or_create_by!(name: 'opiyo', age: 30)
  Hoge Load (0.3ms)  SELECT  "hoges".* FROM "hoges" WHERE "hoges"."name" = $1 AND "hoges"."age" = $2 LIMIT $3  [["name", "opiyo"], ["age", 30], ["LIMIT", 1]]
=> #<Hoge:0x00007fe9b6190788 id: 1, name: "opiyo", age: 30, gender: nil, birthday: Fri, 04 Aug 1989, email: nil, created_at: Mon, 30 Sep 2019 20:47:43 JST +09:00, updated_at: Mon, 30 Sep 2019 20:47:43 JST +09:00>

> Hoge.find_or_create_by!(name: 'opiy', age: 40)
  Hoge Load (0.4ms)  SELECT  "hoges".* FROM "hoges" WHERE "hoges"."name" = $1 AND "hoges"."age" = $2 LIMIT $3  [["name", "opiy"], ["age", 40], ["LIMIT", 1]]
   (0.2ms)  BEGIN
  Hoge Create (0.3ms)  INSERT INTO "hoges" ("name", "age", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "opiy"], ["age", 40], ["created_at", "2019-10-01 13:27:21.287452"], ["updated_at", "2019-10-01 13:27:21.287452"]]
   (4.0ms)  COMMIT
=> #<Hoge:0x00007fe9d17bc358 id: 3, name: "opiy", age: 40, gender: nil, birthday: nil, email: nil, created_at: Tue, 01 Oct 2019 22:27:21 JST +09:00, updated_at: Tue, 01 Oct 2019 22:27:21 JST +09:00>

これ以外にも基本的に上記で紹介した処理は繋げて書くことが可能です。

> Hoge.find_by(name: 'opiyo')
  Hoge Load (0.5ms)  SELECT  "hoges".* FROM "hoges" WHERE "hoges"."name" = $1 LIMIT $2  [["name", "opiyo"], ["LIMIT", 1]]
=> #<Hoge:0x00007fe9d4811508 id: 1, name: "opiyo", age: 30, gender: nil, birthday: Fri, 04 Aug 1989, email: nil, created_at: Mon, 30 Sep 2019 20:47:43 JST +09:00, updated_at: Mon, 30 Sep 2019 20:47:43 JST +09:00>

> Hoge.find_by(name: 'opiyo').update_attributes(gender: '男性')
  Hoge Load (0.4ms)  SELECT  "hoges".* FROM "hoges" WHERE "hoges"."name" = $1 LIMIT $2  [["name", "opiyo"], ["LIMIT", 1]]
   (0.2ms)  BEGIN
  Hoge Update (0.5ms)  UPDATE "hoges" SET "gender" = $1, "updated_at" = $2 WHERE "hoges"."id" = $3  [["gender", "\xE7\x94\xB7\xE6\x80\xA7"], ["updated_at", "2019-10-01 13:29:00.694154"], ["id", 1]]
   (0.7ms)  COMMIT
=> true

> Hoge.find_by(name: 'opiyo')
  Hoge Load (0.6ms)  SELECT  "hoges".* FROM "hoges" WHERE "hoges"."name" = $1 LIMIT $2  [["name", "opiyo"], ["LIMIT", 1]]
=> #<Hoge:0x00007fe9b8121070 id: 1, name: "opiyo", age: 30, gender: "男性", birthday: Fri, 04 Aug 1989, email: nil, created_at: Mon, 30 Sep 2019 20:47:43 JST +09:00, updated_at: Tue, 01 Oct 2019 22:29:00 JST +09:00>

ざっとよく使われるものを紹介してきましたが、どんな場面でどのように使うのか。こんな時はどうするのかなど一杯あると思いますのでまた別の機会で、 深掘りした内容もまとめていきたいと思います。

【Rails】モデルの変更前後の値や変更されたのかをチェックする

Ruby on Railsで保存や変更した時の値を取得したり、値が変更されたのかどうかをチェックする方法を紹介します。

変更のチェック(changed?)

モデル全体で変更があるかどうかをチェックするにはchanged?を使います。

> hoge = Hoge.last
  Hoge Load (0.3ms)  SELECT  "hoges".* FROM "hoges" ORDER BY "hoges"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> #<Hoge:0x00007fd646062b78 id: 1, name: "opiyo", age: 30, gender: nil, birthday: Fri, 04 Aug 1989, email: nil, created_at: Mon, 30 Sep 2019 20:47:43 JST +09:00, updated_at: Mon, 30 Sep 2019 20:47:43 JST +09:00>

> hoge.name = 'opiyoopiyo'
=> "opiyoopiyo"

> hoge.changed?
=> true

特定のカラムの変更をチェック(column_changed?)

特定のカラムのみが変更されたかどうかをチェックするにはchanged?の前にカラム名を付けてカラム名_changed?にします。

> hoge = Hoge.last
=> #<Hoge:0x00007fd646062b78 id: 1, name: "opiyo", age: 30, gender: nil, birthday: Fri, 04 Aug 1989, email: nil, created_at: Mon, 30 Sep 2019 20:47:43 JST +09:00, updated_at: Mon, 30 Sep 2019 20:47:43 JST +09:00>

> hoge.name = 'opiyoopiyo'
=> "opiyoopiyo"

> hoge.name_changed?
=> true

# ageは変更してないので`false`になる
> hoge.age_changed?
=> false

変更前の値を取得する(column_was)

変更前の値についてはカラム名の後に_wasを付けます。

> hoge.name
=> "opiyo"

> hoge.name = 'opiyoopiyo'
=> "opiyoopiyo"

> hoge.name_was
=> "opiyo"

使い方と注意点

利用する場面としてはbefore_saveが多いでしょうか。

# hoge.rb
class Hoge < ApplicationRecord
  before_save :adult?, if: Proc.new { self.birthday_changed? }

  def adult?
    self.age > 20
  end
end

最大の注意点としてはafter_saveなどのafter処理で利用すると正しく取得できないので、必ずbefore処理で利用するようにしましょう!