Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
12
Help us understand the problem. What is going on with this article?

More than 1 year has passed since last update.

@dy36

[Rails] ActiveModel::Attributesの使い方(配列化やネストしたhashの取り扱いなども)

ActiveModel::Attributesの使い方について使い方をまとめてみたいと思います。他の方の記事も少しだけありましたが、配列の作り方とか、ネストしているhashをオブジェクト化するやり方があまりまとまってなかったので初投稿してみることにしました。

サマリ

記事の下まで読むと、ネストしているhashをActiveModel::Attributesを使ってきれいにオブジェクト化することができます。こんな感じです。

hash = {name: "徒然草", price: 1000, authors: [{name: "兼好法師", age: 35}, {name: "健康男児", age: 20}]}
book = Book.new(hash)
book.name
# => "徒然草"
book.authors.class
# => Array
book.authors.first.class
# => Author
book.authors.first.name
# => "兼好法師"
book.authors.first.age
# => 35

なお、ActiveModel::Attributesとはなにかについては触れません。あとバリデーションについても触れてません。

この記事で書くこと

  • 基本的な書き方
  • hashを渡してインスタンス生成
  • カスタムタイプを使う
  • array型を作る
  • ネストしたインスタンスを生成する

基本的な書き方

ActiveModel::Attributesを使うためには自分で作ったclassにActiveModel::Modelと一緒にincludeして使います。

class Book
  include ActiveModel::Model
  include ActiveModel::Attributes
end

属性(attribute)を定義する

クラスに持たせる属性を定義します。

class Book
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :price, :integer, default: 100
  attribute :createdate, :datetime
end

defaultを設定すると、インスタンス生成した際のデフォルト値を設定できます。

標準で使用できる型

:stringなどの型っぽいのがシンボルで渡されているけど、これはなに?」と思うと思います。これはActiveModel::Typeのなかで事前に定義されている型です。ActiveModel::Attributesでは、ActiveModel::Typeの型を使う必要があります。基本的な型はそろっていますが、これ以外の型を使いたい場合はカスタムタイプを自分で定義する必要があります。(後述)
デフォルトで使える方は以下の通り。

:big_integer -> BigInteger型
:binary -> Binary型
:boolean -> Boolean型
:date -> Date型
:datetime -> DateTime型
:decimal -> Decimal型
:float -> Float型
:immutable_string -> ImmutableString型
:integer -> Integer型
:string -> String型
:time -> Time型

Railsのコードのこちらにregisterメソッドで定義しています。

hashを渡してインスタンス生成

基本的な使い方はこのclassにhashを渡してあげてインスタンスを生成する形です。JSONデータをパースしてそのまま放り込むとオブジェクトが生成できるので便利です。

class Book
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :price, :integer, default: 100
  attribute :createdate, :datetime
end

book = Book.new({name: "徒然草", price: 200, createdate: '1993-02-24T12:30:45'})
book.class # => Book

ちゃんとBookクラスのインスタンスが生成できています。attributeは.をつけて呼び出せます。

book.name # => "徒然草"
book.price # => 200
book.createdate # => 1993-02-24 12:30:45 UTC
book.createdate.class # => Time

ここでdefaultも試してみます。priceをhashに含めないでインスタンスを生成すると

book = Book.new({name: "徒然草", createdate: '1993-02-24T12:30:45'})
book.price # => 100

ちゃんとデフォルト値が入ります。逆にattributeにないものを入れようとすると、

book = Book.new({name: "徒然草", createdate: '1993-02-24T12:30:45'}, author: "aa")
# ArgumentError (wrong number of arguments (given 2, expected 0..1))

エラーになります。

attributesメソッド、attribute_namesメソッド

attributesメソッドで定義されている属性全てがhashで取得できます。

book.attributes
# => {"name"=>"徒然草", "price"=>100, "createdate"=>1993-02-24 12:30:45 UTC}

また、attribute_namesで属性の名前だけを配列で取得できます。(Rails 6.0.0以降の場合のみ)

book.attribute_names
# => ["name", "price", "createdate"]

カスタムタイプを使う

ここから応用編です。デフォルトで用意されている型ではないものを使いたい時はカスタムタイプを定義します。例えば以下のようなauthor属性を追加してみたいとします。

class Book
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :price, :integer, default: 100
  attribute :createdate, :datetime
  attribute :author_name, :author
end

:authorは元々定義されているものではないので自分で型を作ります。

models/type_author.rb
# models/type_author.rbを新規作成
class TypeAuthor < ActiveModel::Type::String
  def cast_value(value)
    "吾輩は" + value + "である"
  end
end
config/initializers/types.rb
# config/initializers/types.rbを新規作成
ActiveModel::Type.register(:author, TypeAuthor)

これで定義完了。実際に使ってみます。

book = Book.new({name: "徒然草", price: 200, createdate: '1993-02-24T12:30:45', author_name: "兼好法師"})
book.author_name # => "吾輩は兼好法師である"

何をしたかと言うと、TypeAuthorクラスはActiveModel::Type::Stringクラスを継承していて、ActiveModel::Type::Stringクラスにあるcast_valueメソッドをオーバーライドしています。
インスタンス生成する際にsetterがこのメソッドを呼び出して、cast_valueメソッドの戻り値が属性にセットされる形になります。

Array型を作る

属性に配列を持たすこともできます。先ほどのTypeBookクラスを改良してみます。ActiveModel::Type::Valueクラスは先程使ったActiveModel::Type::Stringクラスの親クラスで、受け取った値をそのままsetする型です。

models/type_author.rb
# models/type_author.rb
class TypeAuthor < ActiveModel::Type::Value
  def cast_value(value)
    value
  end
end
config/initializers/types.rb
ActiveModel::Type.register(:author, TypeAuthor)

単純に受け取った値をそのまま返すだけにして、author_nameauthor_namesにして配列を受け取ります。この状態でauthor_namesに配列を渡してみるとどうなるでしょうか

class Book
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :price, :integer, default: 100
  attribute :createdate, :datetime
  attribute :author_names, :author
end

book = Book.new({name: "徒然草", price: 200, createdate: '1993-02-24T12:30:45', author_names: ["兼好法師", "健康男児"]})
book.author_names
# => ["兼好法師", "健康男児"]

ちゃんと配列が格納されています。

ネストしたインスタンスを生成する

最後に属性に別のインスタンスの配列を持たせるような形にできないでしょうか。例えば、Bookクラスの属性としてAuthorクラスの配列が含まれているもの。hashにするとこんな元データです。APIで受け取ったJSONデータとかよくこのような形になっていますよね。

{
  name: "徒然草",
  price: 1000,
  authors: [
    {name: "兼好法師", age: 35},
    {name: "健康男児", age: 20}
  ]
}

兼好法師と健康男児はAuthorクラスのオブジェクトとして生成し、authorsの属性として配列で持つというケースです。これはどうやってやるかというと、今までの説明の全てを駆使してやります。

models/book.rb
class Book
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :price, :integer, default: 100
  attribute :authors, :author_array, default: []
end
models/author.rb
class Author
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :age, :integer
end
models/type_author_array.rb
class TypeAuthorArray < ActiveModel::Type::Value
  def cast_value(value)
    arr = []
    value.each do |v|
      arr.push Author.new(v)
    end
    arr
  end
end

# config/initializers/types.rb
ActiveModel::Type.register(:author_array, TypeAuthorArray)

ポイントとしては型のキャストをするTypeAuthorArrayクラスでAuthorクラスのインスタンスを生成して、配列に格納してBookクラスのインスタンスにセットしてあげる部分です。
早速使ってみましょう。

hash = {name: "徒然草", price: 1000, authors: [{name: "兼好法師", age: 35}, {name: "健康男児", age: 20}]}
book = Book.new(hash)
book.authors.class
# => Array
book.authors.first.class
# => Author
book.authors.first.name
# => "兼好法師"

素晴らしい。ちゃんとauthors属性にAuthorクラスのインスタンスの配列が格納されていて、Authorクラスの属性を呼んであげると、値が取得できました。

最後に

APIでJSONファイルを受け取った後、Modelのオブジェクトと同じように扱いたい場合はActiveModel::Attributeでインスタンスを作ってあげればいろんなことがよしなにできるようになります。

誰かの一助となれば幸いです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
12
Help us understand the problem. What is going on with this article?