|
Passing Objects into Model using Virtual Attributes
By: Bruce Bahlmann - Contributing Author (your
feedback
is important to us!)
In rails development, you may encounter a need to pull in other information
into your model which you need to more fully process the data before saving.
An example of this is common when you call upon a model to log changes to a
particular set of data. In this case, you would want to be able to compare
previously stored values with those being submitted by the user and possibly saving to the database.
There are some suggestions to use before_save or after_validation filters to
make this happen. However, regardless of how you implement this, how do
get the object (the previously stored version of the record being updated)
into the model to allow you to run a comparison?
Fortunately, Ruby on Rails (RoR)
provides an easy way to do this that you don't have to worry about problems
with simultaneous interactive sessions or what you are using for an
application server or web server. What I believe is the easiest way to pass
additional information into models is to use virtual attributes. Here is
how it works:
[app/models/inventory.rb]
class Inventory < ActiveRecord::Base
attr_accessor :cUser
attr_accessor :pInventory
...
First you define a virtual attribute within your model. Note, you do not
need to create a method for it, just simply define it as above. In this
example I have defined two virtual attributes that will be required to carry
out proper application logging - the pInventory (representing the previously
stored inventory) and the cUser (representing the currently logged in user -
the person making the changes).Keep in mind, that
virtual attributes are just that - virtual. While you may attach or
associate them with objects whose contents may be committed to a database,
the virtual attributes will NOT be committed. Virtual attributes do not
pertain to columns in a database.
[app/controllers/inventory.rb]
...
# POST /inventories
# POST /inventories.json
def create
@inventory = Inventory.new(params[:inventory])
## Send model virtual attributes (these are not committed to the database - simply used to log activity
@inventory.cUser = @current_user.name
@inventory.pInventory = @inventory.to_json
...
# PUT /inventories/1
# PUT /inventories/1.json
def update
@inventory = Inventory.find(params[:id])
## Send model virtual attributes (these are not committed to the database - simply used to log activity
params[:inventory]["cUser"] = @current_user.name
params[:inventory]["pInventory"] = @inventory.to_json
...
Now that the virtual attributes are defined, we need to give each attribute
a value some place where the value is available. There are two areas within
the controller that we want to populate pInventory and cUser - create and update. Both
actions involve processing form information submitted by the user. The
objective here is to assign pInventory and cUser values just prior to their being
handed to a model where verification and saving happen - this prevents
simultaneous interactive sessions from overwriting this value.
In the particular case of pInventory, it is not a simple matter of
passing a string. Rather, pInventory represents an object so to pass this to
the model we apply one simple piece of magic (serialize the object using
JSON) so that it can be sent on to the model embedded similarly with other
fields.
[app/models/inventory.rb]
class Inventory < ActiveRecord::Base
attr_accessor :cUser
...
validate :custom_validation
def custom_validation
if errors.count > 0
## Any post processing of errors prior to returning back to edit/new page
else
## Proceed with any finished processing prior to saving
self.purchaseDate = Time.parse(purchaseDate.to_s).to_i
pi = ActiveSupport::JSON.decode(pInventory)
modified = 1
## Process notes
time = Time.now.to_i
if pi["nKeyID"].to_s == ""
## Unique to NEW (create) Inventory
if notes == ''
self.notes = "#{time}|#{cUser}|Inventory first entered"
else
self.notes = "#{time}|#{cUser}|#{notes}|_|#{time}|#{cUser}|Inventory first entered"
end
else
## Compare previous/current values for changes (update in notes) - Unique to updating Inventory
comparisonText = ''
pi.each do |k,v|
next if k == 'notes'
next if k == 'lmodified'
comparisonText += ", #{k} changed from [#{pi[k]}] to [#{self.send(k)}]" if pi[k].to_s != self.send(k).to_s
end
comparisonText.gsub!(/^\,\s/,'')
## Build notes based on changes detected
if notes == ''
if comparisonText == ''
## No change - record untouched
self.lmodified = pi["lmodified"]
self.notes = pi["notes"]
modified = 0
else
self.notes = "#{time}|#{cUser}|#{comparisonText}|_|#{pi["notes"]}"
end
else
if comparisonText == ''
self.notes = "#{time}|#{cUser}|#{notes}|_|#{pi["notes"]}"
else
self.notes = "#{time}|#{cUser}|#{notes}|_|#{time}|#{cUser}|#{comparisonText}|_|#{pi["notes"]}"
end
end
end
self.lmodified = time if modified == 1
...
Finally, back in the model, we can now work with the values we assigned
pInventory and cUser in the controller with confidence that their values will not be
overwritten. Note that I used decode to unserialize the passed JSON
structure - this allows full use of the passed object (albeit now it is a
hash). We also must use the send method to access elements within the object when we
are looping through the object's attributes to compare previous and current
for any change that we want to note in the log relative the user who made
that change.
It is worth mentioning, that one can approach logging different ways.
Rails seems to more naturally prefer logging that is separate from the data
it is referencing (in other words a separate table that uniquely provides
logging). Perhaps I am a bit old school, but I much prefer logging that is
incorporated within the record it represents. If you have this incorporated
logging, you need to do something like the above as rails scaffolding isn't
going to build this for you.
Can Birds-Eye.Net help you or your Company?
Receive your Birds-Eye.Net articles and white
papers hot off
the presses by adding our RSS feed to your reader.
|
|