问题
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?
回答1:
Here's my take. I haven't tested it with ActiveRecord, but I left comments. Hope this helps.
class Event < ActiveRecord::Base
attr_accessible :end_at, :start_at, :start_time, :end_time, :event_date
attr_accessor :start_time, :end_time, :event_date
def start_time
@start_time || time_attr_from_datetime(start_at)
end
def start_time=(start_time_value)
@start_time = start_time_value
set_start_at
end
def end_time
@end_time || time_attr_from_datetime(end_at)
end
def end_time=(end_time_value)
@end_time = @end_time_value
set_end_at
end
def event_date
@event_date || start_at.to_date.to_s(:db)
end
def event_date=(event_date_value)
@event_date = event_date_value
set_start_at
set_end_at
end
def start_at=(start_at_value)
write_attribute(:start_at, start_at_value) # Maybe you need to do write_attribute(:start_at, DateTime.parse(start_at_value)) here ???
@start_time = time_attr_from_datetime(start_at)
end
def end_at=(end_at_value)
write_attribute(:end_at, end_at_value) # Maybe you need to do write_attribute(:end_at, DateTime.parse(end_at_value)) here ???
@end_time = time_attr_from_datetime(end_at)
end
private
def set_start_at
self.start_at = DateTime.parse("#{event_date} #{start_time}:00")
end
def set_end_at
self.end_at = DateTime.parse("#{event_date} #{end_time}:00")
end
def time_attr_from_datetime(datetime)
"#{'%02d' % datetime.hour}:#{'%02d' % datetime.min}"
end
end
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.
回答2:
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.
def event_date
start_at.to_date.to_s(:db) # yyyy-mm-dd
end
def start_time
"#{'%02d' % start_at.hour}:#{'%02d' % start_at.min}"
end
def end_time
"#{'%02d' % end_at.hour}:#{'%02d' % end_at.min}"
end
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)
来源:https://stackoverflow.com/questions/12080868/setting-getting-virtual-attributes-in-rails-model