[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を新規作成
class TypeAuthor < ActiveModel::Type::String
  def cast_value(value)
    "吾輩は" + value + "である"
  end
end
# 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
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 でインスタンスを作ってあげればいろんなことがよしなにできるようになります。

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

Back