[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_name
をauthor_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 でインスタンスを作ってあげればいろんなことがよしなにできるようになります。
誰かの一助となれば幸いです。