I'm looking for a rails-y way to approach the following:
Two datetime
attributes in an Event model:
start_at: datetime
end_at: datetime
I would like to use 3 fields for accessing them in a form:
event_date
start_time
end_time
The problem I'm having is how to keep the actual and the virtual attributes in "sync" so the model can be updated via the form and/or directly via start_at
& end_at
.
class Event < ActiveRecord::Base
attr_accessible :end_at, :start_at, :start_time, :end_time, :event_date
attr_accessor :start_time, :end_time, :event_date
after_initialize :get_datetimes # convert db format into accessors
before_validation :set_datetimes # convert accessors into db format
def get_datetimes
if start_at && end_at
self.event_date ||= start_at.to_date.to_s(:db) # yyyy-mm-dd
self.start_time ||= "#{'%02d' % start_at.hour}:#{'%02d' % start_at.min}"
self.end_time ||= "#{'%02d' % end_at.hour}:#{'%02d' % end_at.min}"
end
end
def set_datetimes
self.start_at = "#{event_date} #{start_time}:00"
self.end_at = "#{event_date} #{end_time}:00"
end
end
Which works:
1.9.3p194 :004 > e = Event.create(event_date: "2012-08-29", start_time: "18:00", end_time: "21:00")
=> #<Event id: 3, start_at: "2012-08-30 01:00:00", end_at: "2012-08-30 04:00:00", created_at: "2012-08-22 19:51:53", updated_at: "2012-08-22 19:51:53">
Until setting actual attributes directly (end_at
set back to end_time
on validation):
1.9.3p194 :006 > e.end_at = "2012-08-30 06:00:00 UTC +00:00"
=> "2012-08-30 06:00:00 UTC +00:00"
1.9.3p194 :007 > e
=> #<Event id: 3, start_at: "2012-08-30 01:00:00", end_at: "2012-08-30 06:00:00", created_at: "2012-08-22 19:51:53", updated_at: "2012-08-22 19:51:53">
1.9.3p194 :008 > e.save
(0.1ms) BEGIN
(0.4ms) UPDATE "events" SET "end_at" = '2012-08-30 04:00:00.000000', "start_at" = '2012-08-30 01:00:00.000000', "updated_at" = '2012-08-22 20:02:15.554913' WHERE "events"."id" = 3
(2.5ms) COMMIT
=> true
1.9.3p194 :009 > e
=> #<Event id: 3, start_at: "2012-08-30 01:00:00", end_at: "2012-08-30 04:00:00", created_at: "2012-08-22 19:51:53", updated_at: "2012-08-22 20:02:15">
1.9.3p194 :010 >
My assumption is that I also need to customize the "actual" attribute's setters but I'm not sure how to do that w/out screwing up default behavior. Thoughts? Perhaps there a more "Rails-y" "callback-y" way to handle this?
I would just not 'cache' the 'virtual' attributes at all, esp if you don't need your virtual ones to be "settable", only "gettable", which is what your example looks like.
As soon as you start cacheing, you have to worry about invalidating the cached values -- you basically have an 'invalidating cached values' problem. There are a couple ways to make your original design work -- but I don't think the calculation being made there is expensive enough to justify the added complexity from memoizing/caching as you are doing. Just provide em on demand, and you don't need to worry about invalidating the cached values.
If you really really want to do what you initially propose, this might get you started: Callback for changed ActiveRecord attributes? (not sure if Rails has changed since that stackoverflow was written though)
Here's my take. I haven't tested it with ActiveRecord, but I left comments. Hope this helps.
EDIT: There's a definite pattern to getting and setting start_time and end_time. It could be abstracted a bit with meta-programming, but I thought that would make the example unclear.