開発環境にdockerを使っている場合のrails-erdのセッティング

こんにちは!kossyです!




さて、今回は開発環境にdockerを使っている場合のrails-erdのセッティング方法について、
ブログに残してみたいと思います。



環境

Ruby 2.6.6
Rails 6.0.3.5
rails-erd 1.6.1
ruby-graphviz
docker desktop 2.3.6
docker-compose 1.27.0



dockerfileの修正

docker環境でrails-erdを使う場合、仮想マシン内にruby-graphvizをインストールしなくてはなりません。

以下のようなdockerfileがあるとします。

FROM ruby:2.6
ENV LANG C.UTF-8

RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs vim

RUN mkdir /app
WORKDIR /app

COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock

RUN bundle install
COPY . /app

このdockerfile内でapt-getしてる行でgraphvizを追記します。

FROM ruby:2.6
ENV LANG C.UTF-8

RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs vim graphviz

RUN mkdir /app
WORKDIR /app

COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock

RUN bundle install
COPY . /app

この状態でコンテナを立ち上げ直しましょう

$ docker-compose build

gemfileを編集します。

group :development do
  gem 'listen', '~> 3.2'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
  gem 'rails-erd' # 追記
end

bundle installします。

$ docker-compose run web bundle install

PDFファイルを生成するには以下のコマンドを実行します。

$ docker-compose run web bundle exec erd

このコマンドを実行することで、アプリケーションのルートディレクトリ(appとかconfigとかtmpと置いてあるディレクトリと同じ階層)に、「erd.pdf」というファイルが生成されます。

蛇足

デフォルトの設定のままだと、外部キーが表示されなかったりするので、オプションを入れてコマンド実行もできます。

# timestampと主キー以外のカラムを表示
$ docker-compose run web bundle exec erd --attributes=content,foreign_keys

より詳しいオプションを知りたい方は以下ドキュメントをご覧ください。
voormedia.github.io




勉強になりました。



大いに参考にさせていただいたサイト

この場を借りて御礼を申し上げます。

Rails ERD – Customisation options
railsでER図を出力する - rails erd | Dendoron

Rails6のHostのホワイトリスト機能の使い方

こんにちは!kossyです!




さて、今回はRails6のHostのホワイトリスト機能の使い方について、ブログに残してみたいと思います。



環境

Ruby 2.6.6
Rails 6.0.3.5
Docker for Mac



Hostのホワイトリスト機能とは?

Hostのホワイトリスト機能が入ったPR

github.com

概要欄の英語を翻訳してみます。

・Introduce guard against DNS rebinding attacks

DNS再バインド攻撃に対するガードを導入します

The ActionDispatch::HostAuthorization is a new middleware that prevent
against DNS rebinding and other Host header attacks.

ActionDispatch :: HostAuthorizationは、これを防ぐ新しいミドルウェアです。 DNSの再バインドやその他のホストヘッダー攻撃に対して。

In other environments Rails.application.config.hosts is empty and no
Host header checks will be done. If you want to guard against header
attacks on production,

他の環境では、Rails.application.config.hostsは空であり、 ホストヘッダーのチェックが行われます。

The host of a request is checked against the hosts entries with the case operator (#===), which lets hosts support entries of type RegExp, Proc and IPAddr to name a few.

リクエストのホストは、ケース付きのホストエントリに対してチェックされます 演算子(#===)。これにより、ホストは正規表現タイプのエントリをサポートできます。 いくつか例を挙げると、ProcとIPAddrです。

A special case is supported that allows you to whitelist all sub-domains:

すべてのサブドメインホワイトリストに登録できる特別なケースがサポートされています。

開発環境の場合は、デフォルトで以下のような設定がされるみたいです。

 Rails.application.config.hosts = [
   IPAddr.new("0.0.0.0/0"), # All IPv4 addresses.
   IPAddr.new("::/0"),      # All IPv6 addresses.
   "localhost"              # The localhost reserved domain.
 ]

コンソールで設定の確認をしてみます。

$ Rails.env
=> "development"

$ Rails.application.config.hosts
=> [".localhost",
 #<IPAddr: IPv4:0.0.0.0/0.0.0.0>,
 #<IPAddr: IPv6:0000:0000:0000:0000:0000:0000:0000:0000/0000:0000:0000:0000:0000:0000:0000:0000>
]

きちんと設定されていることがわかります。

本番環境の場合はhostsは空になっているので、自身で設定する必要があります。

# config/environments/production.rb

config.hosts << "example.com"

DNS rebinding attackについて

wikipedia先生に頼ります。
en.wikipedia.org

DNS再バインドは、コンピュータ攻撃の一形態として一般的に使用されるドメイン名の解決を操作する方法です。この攻撃では、悪意のあるWebページにより、訪問者はネットワーク上の他の場所のマシンを攻撃するクライアント側のスクリプトを実行します。理論的には、同一生成元ポリシーはこれが発生しないようにします。クライアント側のスクリプトは、スクリプトを提供したのと同じホスト上のコンテンツにのみアクセスできます。ドメイン名の比較は、このポリシーを適用する上で不可欠な部分であるため、DNSの再バインドは、ドメインネームシステム(DNS)を悪用することにより、この保護を回避します。

この攻撃は、被害者のWebブラウザにプライベートIPアドレスのコンピュータにアクセスさせ、その結果を攻撃者に返すことにより、プライベートネットワークを侵害するために使用される可能性があります。また、スパム、分散型サービス拒否攻撃、またはその他の悪意のあるアクティビティに被害者のマシンを使用するために使用することもできます。

攻撃者はドメイン(attacker.comなど)を登録し、攻撃者の制御下にあるDNSサーバーに委任します。サーバーは、非常に短い存続時間(TTL)レコードで応答するように構成されているため、DNS応答がキャッシュされません。被害者が悪意のあるドメインを参照すると、攻撃者のDNSサーバーは、最初に悪意のあるクライアント側のコードをホストしているサーバーのIPアドレスで応答します。たとえば、被害者のブラウザを、被害者のコンピュータで実行することを目的とした悪意のあるJavaScriptまたはFlashスクリプトを含むWebサイトに向けることができます。

悪意のあるクライアント側のコードは、元のドメイン名(attacker.comなど)に追加のアクセスを行います。これらは同一生成元ポリシーで許可されています。ただし、被害者のブラウザがスクリプトを実行すると、ドメインに対して新しいDNS要求が行われ、攻撃者は新しいIPアドレスで応答します。たとえば、インターネット上の別の場所にあるターゲットの内部IPアドレスまたはIPアドレスで応答できます。

シスコのドキュメントによると、DNSのフィルタリングを行うことで、攻撃から保護できるとのことでした。
umbrella.cisco.com

Rails6のHostのホワイトリスト機能の場合、偽造されたHostが指定された場合でも、許可されたホストの通信のみ許可する設定にしているため、
DNS rebinding Attackを防げるということかと思います。

Hostを偽装してアクセスしてみる

curlを使ってHostを偽装して開発環境に対してリクエストをしてみます。

$ curl -v -H 'Host:yhaoo.cp.jp' http://localhost:3000/

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1
> Host:yahoo.co.jp
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 403 Forbidden
< Content-Type: text/html; charset=UTF-8
< Content-Length: 3108
<

# 省略

* Connection #0 to host localhost left intact
* Closing connection 0

403が返ってきました。省略した部分はHTMLで、添付画像のような内容でした。
f:id:kossy-web-engineer:20210227134550p:plain

表示されている通り、Hostの指定をして再度リクエストを試みてみます。

$ curl -v -H 'Host:yahoo.co.jp' http://localhost:3000/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET / HTTP/1.1
> Host:yahoo.co.jp
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Frame-Options: SAMEORIGIN
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< X-Download-Options: noopen
< X-Permitted-Cross-Domain-Policies: none
< Referrer-Policy: strict-origin-when-cross-origin
< Content-Type: text/html; charset=utf-8
< ETag: W/"8a65edeb7c0d064821d16acc080a28c2"
< Cache-Control: max-age=0, private, must-revalidate
< X-Request-Id: 62f6b96e-407b-4f65-bbd6-9158f8c0fc33
< X-Runtime: 0.478029
< Transfer-Encoding: chunked

HTTPステータス200が返ってきました。

Hostの偽装が可能なこととHostの指定によって異なるホストへの通信が許可されることがわかりました。




勉強になりました。



大いに参考にさせていただいたサイト

この場を借りて御礼を申し上げます。

Guard against DNS rebinding attacks by permitting hosts by gsamokovarov · Pull Request #33145 · rails/rails · GitHub
DNS rebinding - Wikipedia

アップロードされたファイルのMime-TypeをチェックできるGem 「marcel」の使い方

こんにちは!kossyです!




さて、今回はアップロードされたファイルの中身をチェックできるGem 「marcel」の使い方について、
ブログに残してみたいと思います。




GitHub - basecamp/marcel: Find the mime type of files, examining file, filename and declared type



Marcel Gemとは?

公式ドキュメントの説明をGoogle翻訳先生に訳してもらいます。

Marcel attempts to choose the most appropriate content type for a given file by looking at the binary data, the filename, and any declared type (perhaps passed as a request header)

Marcelは、バイナリデータ、ファイル名、および宣言されたタイプ(おそらく要求ヘッダーとして渡される)を調べて、特定のファイルに最も適切なコンテンツタイプを選択しようとします。

これは使い方まで見ないといまいち理解できなさそうです、、、

Marcel::MimeType.for Pathname.new("example.gif")
#  => "image/gif"

File.open "example.gif" do |file|
  Marcel::MimeType.for file
end
#  => "image/gif"

Marcel::MimeType.for Pathname.new("unrecognisable-data"), name: "example.pdf"
#  => "application/pdf"

Marcel::MimeType.for extension: ".pdf"
#  => "application/pdf"

Marcel::MimeType.for Pathname.new("unrecognisable-data"), name: "example", declared_type: "image/png"
#  => "image/png"

Marcel::MimeType.for StringIO.new(File.read "unrecognisable-data")
#  => "application/octet-stream"

forメソッドにファイルのパスを渡すと、そのファイルのMime-Typeを返すみたいですね。

さらにドキュメントを読んでみます。

By preference, the magic number data in any passed in file is used to determine the type. If this doesn't work, it uses the type gleaned from the filename, extension, and finally the declared type.

If no valid type is found in any of these, "application/octet-stream" is returned.

Some types aren't easily recognised solely by magic number data. For example Adobe Illustrator files have the same magic number as PDFs (and can usually even be viewed in PDF viewers!).

For these types, Marcel uses both the magic number data and the file name to work out the type:

優先的に、渡されたファイルのマジックナンバーデータを使用してタイプが決定されます。これが機能しない場合は、ファイル名、拡張子、最後に宣言された型から収集した型を使用します。

これらのいずれにも有効なタイプが見つからない場合は、「application / octet-stream」が返されます。

一部のタイプは、マジックナンバーデータだけでは簡単に認識できません。たとえば、Adobe IllustratorファイルのマジックナンバーはPDFと同じです(通常はPDFビューアで表示することもできます)。

これらのタイプの場合、Marcelはマジックナンバーデータとファイル名の両方を使用してタイプを計算します。

だいぶわかってきました。

例えばファイルアップロードの機能を実装する場合、拡張子に応じて制限をかけることはよくあると思うのですが、
拡張子だけで制限をかけても不十分で、きちんとファイルの中身まで見に行って制限をする必要がある場合に、Mime-Typeを見てくれるMarcelが役に立つわけですね。

# 以下は実装イメージです

mime_type = Marcel::MimeType.for(params[:upload_file])

if ["image/jpeg", "image/png", "application/pdf"].include?(mime_type)
  # 保存する処理
else
  raise "UnPermittedMimeTypeError"
end

ちなみにMarcelはrails newするとデフォルトでインストールされるようになっています。

    activestorage (6.0.3.5)
      actionpack (= 6.0.3.5)
      activejob (= 6.0.3.5)
      activerecord (= 6.0.3.5)
      marcel (~> 0.3.1)

ActiveStorageが依存しているようです。
RubyDoc公式にも記載がありました。

rubygems.org

ソースコードだとこの辺りですね。

rails/blob.rb at main · rails/rails · GitHub

    def extract_content_type(io)
      Marcel::MimeType.for io, name: filename.to_s, declared_type: content_type
    end

あとはここ

rails/identifiable.rb at 291a3d2ef29a3842d1156ada7526f4ee60dd2b59 · rails/rails · GitHub

    def identify_content_type
      Marcel::MimeType.for download_identifiable_chunk, name: filename.to_s, declared_type: content_type
    end

どちらもprivateメソッドになっているので、普通に使う分にはMarcelを使っていることを意識することはなさそうです。




勉強になりました。



railsでメソッドの定義場所をコンソールから確認してみた

こんにちは!kossyです!




さて、今回はRailsでメソッドの定義場所を調べる方法について、ブログに残してみたいと思います。



環境

Ruby 2.6.6
Rails 5.2.4
Docker for mac



ActionView::Base#renderの定義場所を調べる

諸事情あってActionView::Base#renderの中身を知る必要がありました。

そこで、

qiita.com

の記事を参考に、定義場所を調べてみました。

controller = ActionController::Base.new
view = ActionView::Base.new(Rails.root.join('app', 'views'), {}, controller)

view.method(:render).source_location
=> ["/var/rails/www/shared/bundle/ruby/2.6.0/gems/actionview-5.2.4.4/lib/action_view/helpers/rendering_helper.rb", 27]

はい。rendering_helper.rbの27行目に定義されているとのこと。

RailsソースコードGithubにアップされているので、Githubにアクセスしてメソッドの中身を確認します。

rails/rendering_helper.rb at 5-2-stable · rails/rails · GitHub

      def render(options = {}, locals = {}, &block)
        case options
        when Hash
          if block_given?
            view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
          else
            view_renderer.render(self, options)
          end
        else
          view_renderer.render_partial(self, partial: options, locals: locals, &block)
        end
      end

見つかりました。ここからGithubの定義元ジャンプ機能を利用してコードを追っていくことができると思います。




勉強になりました。



大いに参考にさせていただいた記事

この場を借りて御礼を申し上げます。

Rubyでメソッドの定義場所を見つける方法 - Qiita
Method#source_location (Ruby 3.0.0 リファレンスマニュアル)

RailsでCOALESCEを使って勤続年数を出してみた

こんにちは!kossyです!




さて、今回はRailsでCOALESCEを使って勤続年数を出してみたので、ブログに残してみたいと思います。




環境

Rails 6.0.3.4
Ruby 2.6.6
PostgreSQL 12
Docker For Mac

前提

以前書いた拙著のテーブル構成を例にとります。

kossy-web-engineer.hatenablog.com


今回はSQLのCOALESCE関数を使って勤続年数を出してみようと思います。

blog.amedama.jp

COALESCE関数は、「いくつかの値の中で最初にNULLでない値」を返すSQL関数です。
また、「NULLの値を任意の値に置き換える」ということもできます。

勤続年数の場合、導出データのため、そのままカラムを用意して保存するというケースはないと思います。
もしカラムを用意して保存するとなると、そのうち更新しなくてはいけなくなるからです。
同じ理由で「年齢」もカラムを用意して保存することはないですね。

導出データについては以下が詳しかったです。
blog.codecamp.jp

勤続年数を算出するには、「入社日」が必要ですが、今回の例ではhired_atがそれに当たります。
社員を管理するようなアプリの場合、退職している方もいらっしゃると思いますから、retired_atというカラムを用意しています。

この時、退職していない社員の方は、retired_atの値がnullのため、特に値を加工しないで勤続年数を求めようとする場合、

null - hired_at

のような計算になってしまい、エラーになってしまいます。

そんな時、COALESCE関数が大活躍します。

  scope :length_of_service, -> () {
    query = <<-SQL
      SELECT id, last_name, first_name,
      (COALESCE(retired_at, current_date) - hired_at) / 365 as length_of_service
      FROM employees
      ORDER BY id ASC
    SQL

    find_by_sql(query)
  }

上記のクエリは、COALESCE関数を使ってretired_atの値をそのまま使うか、nullであればPostgreSQLのcurrent_date関数で、当日の日付を使っています。
なので、retired_atかcurrent_date - hired_atという計算がどの行のレコードでも行うことができます。

COALESCE関数、シンプルですがかなり強力な関数ですね。




勉強になりました。



RailsでSQL直書きでCASE文を書いてみる

こんにちは!kossyです!




さて、今回はRailsSQL直書きでCASE文を書いてみたので、ブログに残してみたいと思います。



環境

Rails 6.0.3.4
Ruby 2.6.6
PostgeSQL 12
Docker For Mac




前提

以前書いた拙著のテーブル構成を例にとります。

kossy-web-engineer.hatenablog.com




ごくごく簡単な例を書いてみました。

  scope :listed_employees, -> () {
    query = <<-SQL
      SELECT id, retired_at,
      CASE
        WHEN retired_at IS NULL THEN last_name || first_name
        ELSE last_name || first_name || '(退職済み)'
      END as name
      FROM employees
      ORDER BY id ASC
    SQL

    find_by_sql(query)
  }

retired_atがNULLでなければ、名前の最後に(退職済み)と記載するようにしています。
そしてヒアドキュメントで書いたクエリをfind_by_sqlメソッドで呼び出しています。

次はフラグをつける例です。

  scope :added_retired_flag_employees, -> () {
    query = <<-SQL
      SELECT id, retired_at,
      CASE
        WHEN retired_at IS NULL THEN 0
        ELSE 1
      END as retire_flag
      FROM employees
      ORDER BY id ASC
    SQL

    find_by_sql(query)
  }

retired_atがnullなら0を、nullでなければ1を出力しています。
このように、CASE文でフラグをつけるような使い方もできます。




勉強になりました。



大いに参考にさせていただいたサイト

この場を借りて御礼を申し上げます。

CASE式で条件分岐をSQL文に任せる - Qiita

graphql-batchでloadするデータにthenメソッドで処理を加える

こんにちは!kossyです!




さて、今回はgraphql-batchのthenメソッドを使って、loadしたobjectに処理を加える方法について、
ブログに残してみたいと思います。



環境

Ruby 2.6.3
Rails 6.0.3.4
graphql 1.11.6
graphql-batch 0.4.3



方法

post has_many comments という関連が組まれているとします。
この場合、 N + 1の発生を防ぐために、以下のようなタイプ定義を行うかと思います。

module Types
  class PostType < Types::BaseObject
    field :id, ID, null: true
    field :user_id, Integer, null: true
    field :title, String, null: true
    field :body, String, null: true
    field :status, Integer, null: true
    field :created_at, GraphQL::Types::ISO8601DateTime, null: true
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: true
    field :comments, [Types::CommentType], null: true

    def comments
      Loaders::AssociationLoader.for(Post, :comments).load(object)
    end
  end
end

graphql-batchのforメソッドとloadメソッドを使って、オブジェクトのloadを行っています。

この時、thenメソッドを使うことで、後続の処理を加えることができます。

以下は、ActiveRecordのorderメソッドを使って、created_atで降順でsortしています。

    def comments
      Loaders::AssociationLoader.for(Post, :comments).load(object).then do |comments|
        comments.order(created_at: :desc)
      end
    end

f:id:kossy-web-engineer:20210214210343p:plain

添付画像はGraphQLのGUIクライアントであるAltairでcallした画像ですが、
created_atで降順でsortされていることがわかります。

また、thenメソッドはチェインして用いることができます。

    def likes
      Loaders::AssociationLoader.for(Post, :comments).load(object).then do |comments|
        Loaders::AssociationLoader.for(Comment, :likes).load(comments).then do |likes|
          likes.order(created_at: :desc)
        end  
      end
    end

上記のソースコードはcommentsについたいいねを降順で並び替えるような例になります。




勉強になりました。



大いに参考にさせていただいたサイト

この場を借りて御礼を申し上げます。

graphql-rubyでmethodオプションを使って、modelに定義したメソッドをfieldにする方法

こんにちは!kossyです!




さて、今回はgraphql-rubyでmethodオプションを使ってmodelに定義したメソッドをfieldにする方法について、ブログに残してみたいと思います。



環境

Ruby 2.6.3
Rails 6.0.3.4
graphql 1.11.6



方法

Userモデルがあるとして、以下のメソッドを定義します。

class User < ApplicationRecord

  def full_name
    last_name + first_name
  end
end

app/graphql/types/user_type.rbは以下のような定義にします。

module Types
  class UserType < Types::BaseObject
    implements Types::TimestampInterface

    field :id, ID, null: false
    field :full_name, String, null: true, method: :full_name
    field :email, String, null: true

methodオプションにUserモデルに定義したfull_nameを渡すことで、そのままfieldとすることができます。




勉強になりました。

graphql-rubyでinterfaceを定義してみる

こんにちは!kossyです!




さて、今回はgraphql-rubyでinterfaceを定義する方法について、ブログに残してみたいと思います。



環境

Ruby 2.6.3
Rails 6.0.3.4
graphql 1.11.6



created_atとupdated_atをinterface化する

ジェネレータを使ってGraphQLのセットアップを行うと、graphql/types/base_interface.rbというファイルが生成されるかと思います。
このファイルをincludeすることによって、interfaceの作成を行うことができます。

例として、created_atとupdated_atをinterfaceにしてみたいと思います。

module Types
  module TimestampInterface
    include Types::BaseInterface

    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end

このファイルを、任意のTypeクラスのファイルでimplementsすることで、明示的にcreated_atとupdated_atのfieldを定義しなくとも、
graphqlから叩けるようになります。

# graphql/types/post_type.rb

module Types
  class PostType < Types::BaseObject
    implements Types::TimestampInterface

    field :id, ID, null: false
    field :user_id, Integer, null: false
    field :title, String, null: false
    field :body, String, null: false
    field :status, Integer, null: false
  end
end

これで試しにGraphqlのGUIクライアントであるAltairで叩いてみます。

f:id:kossy-web-engineer:20210212234729p:plain

PostTypeに明示的にfield定義していなくとも叩くことができてます。




勉強になりました。

graphql-rubyでモデルのカラムの値を加工して返したい

こんにちは!kossyです!




さて、今回はgraphql-rubyでカラムの値をよしなに加工して返す方法について、ブログに残してみたいと思います。



環境

Ruby 2.6.3
Rails 6.0.3.4
graphql 1.11.6
graphql-batch 0.4.3



Typeファイル内にfield名と同じ名前の変数を定義する

以下のようなType定義をしているとします。

module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :provider, String, null: false
    field :uid, String, null: false
    field :sign_in_count, Integer, null: false
    field :current_sign_in_at, GraphQL::Types::ISO8601DateTime, null: true
    field :name, String, null: true
    field :nickname, String, null: true
    field :image, String, null: true
    field :email, String, null: true
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
    field :posts, [Types::PostType], null: true
    field :comments, [Types::CommentType], null: true

    def posts
      Loaders::AssociationLoader.for(User, :posts).load(object)
    end

    def comments
      Loaders::AssociationLoader.for(User, :comments).load(object)
    end
  end
end

この時に、例えばemailの@以降の文字列からドメイン提供元の会社名が知りたいとします。

この場合、以下のようにfieldと同名の変数を定義することで、変数の計算結果を返すことができます。

module Types
  class UserType < Types::BaseObject
    # 省略

    field :email_domain, String, null: true

    def email_domain
      return nil if object.email.blank?

      domain = object.email.split('@').last

      case domain
      when 'gmail.com'
        'Google'
      when 'ezweb.ne.jp'
        'au'
      when 'docomo.ne.jp'
        'Docomo'
      when 'i.softbank.jp'
        'ソフトバンク'
      end
    end

    # 省略
  end
end

カラム以外の値を返したい時なんかに使えるやり方かと思います。




勉強になりました。



大いに参考にさせていただいたサイト

この場を借りて御礼を申し上げます。

spirits.appirits.com