I want to add virtual fields / attributes to a rails model and have it included in the attributes that get returned by default when I call @my_model
Assuming I have a model called Booking that has a start_at
and end_at
that are both datetime fields.
@booking = Booking.new(start_at: DateTime.now, end_at: DateTime.now 4.hours)
When I print the booking or say render it as json etc I get the following:
p @booking
# Booking
id => 1,
start_at => Wed, 30 Nov 2022 10:50:20 0800,
end_at => Wed, 30 Nov 2022 14:50:22 0800
All standard so far.
Now I want to add virtual attributes called start_date, start_time, end_date, end_time
that will be derived from the start_at
and end_at
fields
So I add them as attributes to the class:
class Booking < ApplicationRecord
attr_accessor :start_date, :start_time, :end_date, :end_time
def start_time
self.start_at.strftime('%H:%M')
end
...etc
end
so now I print the model again:
@booking
# Booking
id => 1,
start_at => Wed, 30 Nov 2022 10:50:20 0800,
end_at => Wed, 30 Nov 2022 14:50:22 0800
The virtual attributes are not shown.
However if I add them to the attributes method I can call @booking.attributes
:
class Booking < ApplicationRecord
attr_accessor :start_date, :start_time, :end_date, :end_time
def start_time
self.start_at.strftime('%H:%M')
end
def attributes
super.merge({'start_time' => start_ime})
end
end
@booking.attributes
# Booking
id => 1,
start_at => Wed, 30 Nov 2022 10:50:20 0800,
end_at => Wed, 30 Nov 2022 14:50:22 0800
start_time => '10:50'
...etc
So my question is how can i modify what attributes get returned by default, so I don't have to call .attributes on the model every time? I would like to just do render json: @booking
and have all attributes (including the virtual ones) included.
Thanks in advance
CodePudding user response:
You can do it by overriding the as_json
method for that model:
class Booking < ApplicationRecord
def as_json(*)
super(methods: %i(start_date start_time end_date end_time))
end
end
CodePudding user response:
You're falling victim to a very common Ruby beginner misconception. attr_accessor
doesn't actually declare attributes because Ruby doesn't actually have declared attributes/properties like for example Java does.
Ruby just has instance variables and all that attr_accessor :start_date
does is to use metaprogramming to create start_date
and start_date=
getter and setter methods that expose the instance variable to the outside.
That doesn't actually mean that Ruby is keeping track that the Booking class has a start_date
property - the instance variable doesn't even exist until you set it and the setter/getter are just plain old methods. Sometimes these are lazily refered to as "virtual attributes" but its actually far from the truth.
Ruby does however provide the basic building blocks needed to build any kind of attribute system you want. Rails has ActiveModel::Attributes
which is the base API and ActiveRecord::Attributes
which is specialized implementation for database table backed models.
Attibutes are a Rails constuct - not a part of the language.
class Booking < ApplicationRecord
attribute :start_at, type: :datetime
end
irb(main):005:0> Booking.new.as_json
=> {"id"=>nil, "created_at"=>nil, "updated_at"=>nil, "start_at"=>nil}
Unlike attr_accessor
this not just creates setters and getters but it also updates a class instance variable which keeps track of the attributes of the class. That's how Rails knows to include "start_at" when you call to_json
.
If you want to add a method which is not an attribute when rendering JSON use the methods:
option:
class Booking < ApplicationRecord
attribute :start_at, type: :datetime
def start_time
start_at.strftime('%H:%M')
end
end
irb(main):013:0> Booking.new(start_at: Time.now).as_json(methods: [:start_time])
=> {"id"=>nil, "created_at"=>nil, "updated_at"=>nil, "start_at"=>"2022-11-30T16:12:02.188 01:00", "start_time"=>"16:12"}
render json: @booking, methods: [:start_at]
But, overriding as_json
in model or doing complex JSON rendering in the controller are both known anti-patterns. Its often a good idea to move JSON serialization into its own layer.