dream
I'd like to keep record of when a user changes their address.
This way, when an order is placed, it will always be able to reference the user address that was used at the time of order placement.
possible schema
users (
id
username
email
...
)
user_addresses (
id
label
line_1
line_2
city
state
zip
...
)
user_addresses_map (
user_id
user_address_id
start_time
end_time
)
orders (
id
user_id
user_address_id
order_status_id
...
created_at
updated_at
)
in sql, this might look something like: [sql]
select ua.*
from orders o
join users u
on u.id = o.user_id
join user_addressses_map uam
on uam.user_id = u.id
and uam.user_address_id = o.user_address_id
join user_addresses ua
on ua.id = uam.user_address_id
and uam.start_time < o.created_at
and (uam.end_time >= o.created_at or uam.end_time is null)
;
edit: The Solution
@KandadaBoggu posted a great solution. The Vestal Versions plugin is a great solution.
snippet below taken from http://github.com/laserlemon/vestal_versions
Finally, DRY ActiveRecord versioning!
acts_as_versioned by technoweenie was a great start, but it failed to keep up with ActiveRecord’s introduction of dirty objects in version 2.1. Additionally, each versioned model needs its own ver开发者_运维问答sions table that duplicates most of the original table’s columns. The versions table is then populated with records that often duplicate most of the original record’s attributes. All in all, not very DRY.
vestal_versions requires only one versions table (polymorphically associated with its parent models) and no changes whatsoever to existing tables. But it goes one step DRYer by storing a serialized hash of only the models’ changes. Think modern version control systems. By traversing the record of changes, the models can be reverted to any point in time.
And that’s just what vestal_versions does. Not only can a model be reverted to a previous version number but also to a date or time!
Use the Vestal versions plugin for this:
Refer to this screen cast for more details.
class Address < ActiveRecord::Base
belongs_to :user
versioned
end
class Order < ActiveRecord::Base
belongs_to :user
def address
@address ||= (user.address.revert_to(updated_at) and user.address)
end
end
Thought I'd add an updated answer. Seems the paper_trail
gem has become the most popular one for versioning in Rails. It supports Rails 4 as well.
https://github.com/airblade/paper_trail
From their readme:
To setup and install:
gem 'paper_trail', '~> 3.0.6'
bundle exec rails generate paper_trail:install
bundle exec rake db:migrate
Basic Usage:
class Widget < ActiveRecord::Base
has_paper_trail
end
For a specific instance of the Widget
class:
v = widget.versions.last
v.event # 'update' (or 'create' or 'destroy')
v.whodunnit # '153' (if the update was via a controller and
# the controller has a current_user method,
# here returning the id of the current user)
v.created_at # when the update occurred
widget = v.reify # the widget as it was before the update;
# would be nil for a create event
I've only played with it but I'm about to start a pretty ambitious site which will require good versioning of certain classes and I've decided to use paper_trail
.
===EDIT====
I have implemented the paper_trail
gem in production at www.muusical.com and it has worked well using the above. The only change is that I am using gem 'paper_trail', '~> 4.0.0.rc'
in my Gemfile
.
From a data architecture point of view, I suggest that to solve your stated problem of
...when an order is placed, it will always be able to reference the user address that was used at the time of order placement.
... you simply copy the person's address into an Order model. The items would be in OrderItem model. I would reformulate the issue as "An order happens at a point in time. The OrderHeader includes all of the relevant data at that point in time."
Is it non-normal?
No, because the OrderHeader represents a point in time, not ongoing "truth".
The above is a standard way of handling order header data and removes a lot of complexity from your schema as opposed to tracking all changes in a model.
--Stick with a solution that solves the real problem, not possible problems--does anyone need a history of the user's changes? Or do you just need the order headers to reflect the reality of the order itself?
Added: And note that you need to know which address was eventually used to ship the order/invoice to. You do not want to look at an old order and see the user's current address, you want to see the address that the order used when the order was shipped. See my comment below for more on this.
Remember that, ultimately, the purpose of the system is to model the real world. In the real world, once the order is printed out and sent with the ordered goods, the order's ship-to isn't changing any further. If you're sending soft goods or services then you need to extrapolate from the easier example.
Order systems are an excellent case where it is very important to understand the business needs and realities--don't just talk with the business managers, also talk with the front-line sales people, order clerks, accounts receivable clerks, shipping dept folks, etc.
You're looking for the acts_as_audited plugin. It provides an audits table and model to be used in place of your map.
To set it up run the migration and add the following to your user address model.
class UserAddress < ActiveRecord::Base
belongs_to :user
acts_as_audited
end
Once you've set it up, all you need to do is define an address method on order. Something like this:
class Order < ActiveRecord::Base
belongs_to :user
attr_reader :address
def address
@address ||= user.user_address.revision_at(updated_at)
end
end
And you can access the users' address at the time of order completion with @order.address
revision_at
is a method added to an audited model by acts_as_audited. It takes a timestamp and reconstructs the model as it was in that point of time. I believe it pieces the revision together from the audits up on that specific model before the given time. So it doesn't matter if updated_at on the order matches a time exactly.
I think this would be as simple as:
Users:
id
name
address_id
UserAddresses:
id
user_id
street
country
previous_address_id
Orders
id
user_id #to get the users name
user_address_id #to get the users address
Then when a user changes their address, you do a sort of "logical delete" on the old data by creating a new UserAddress, and setting the "previous_address_id" field to be the pointer to the old data. This removes the need for your map table, and creates a sort of linked list. In this way, whenever an order is placed, you associate it to a particular UserAddress which is guaranteed never to change.
Another benefit to doing this is that it allows you to following the changes of a users address, sort of like a rudimentary logger.
精彩评论