views:

409

answers:

4

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 versions 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!

A: 

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.

Mike Trpcic
+1  A: 

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.

EmFi
this looks like a very elegant solution. However, in the Order#address method, you're using `revision_at(updated_at)`-- what if the updated_at doesn't match the user_address updated_at field in the audit table? Will it not find the address?
macek
I'm looking for good documentation for acts_as_audited and I can't seem to find it. Link please?
macek
Unfortunately, the documentation for acts_as_audited is severely lacking. The only reason I know about revision_at is because I've gone digging through the source of acts_as_audited when I forked the repository to add a feature I wanted. Maybe I'll document the rest of it some day. For now I've updated the solution to answer your question about matching timestamps.
EmFi
I've been poking around with this for a while now and I found that you can generate your own docs using `rake doc:plugins:acts_as_audited`. I got plugin this setup but it's not generating the most reliable results. E.g., if I update `foo.bar` from `A` to `B`, using `.revision_at` I will not always get the appropriate value. Using Sequel Pro, I can see the serialized change in the audits table, but re-assembling the model at a specific point just doesn't seem to work 100%. This is sad because this would be perfect otherwise...
macek
EmFi, KandadaBoggu mentioned the __vestal_versions__ plugin below. It operates almost identical to acts_as_audited but seems be function more reliably. To all __acts_as_audited__ users, it's probably worth checking out.
macek
+1  A: 

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.

Larry K
This is a very informative answer and one I was possibly considering. I will likely be doing this for the `Item` details. E.g., `item.price`, `item.cost`, `item.delivery_cost`, `item.supplier_id`, etc will be copied `Order` fields. The difference here is that admins will be making changes to item details and it's less likely that we'd have to go digging into revision history. I guess I don't want to see the `Order` table get so inflated with so many columns. Does this make sense?
macek
I hear you about a lot of columns. This is a tricky issue because orders are essentially legal documents. They need to be under change control, with a history. But each order starts out with its *own* history, starting at time of order creation. The order's ship to changes / history are *not* the same as the User record's history. They're two different things. It is not by chance that the plugin for tracking changes is called "...audited" And remember to also track *who* is making each change to any of the order's data.
Larry K
+3  A: 

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
KandadaBoggu
KandadaBoggu, I got this setup and it looks like it's working great. However, I'm having trouble with the `.at` method. I'm getting `NoMethodError: undefined method `at' for #<Item:0x000001011c9070>`. in `script/console` I'm can test a lot of the other `vestal_versions` methods but for some reason that method is unavailable. What gives?
macek
Try `user.address.versions.at(updated_at)` OR `user.address.version.at(updated_at)`. It looks like the methods in the '/lib/vestal_versions/versions.rb' file are association methods for `versions`.
KandadaBoggu
I'll give this a shot. You should make the update in your post, too! Thanks again :)
macek
I have made the correction to the answer.
KandadaBoggu
I have fixed the response as my code had a bug.
KandadaBoggu
I tried (e.g.) `foo.versions.at(4.day.ago)` and I get back `#<VestalVersions::Version:0x000001020ec2f0>`. However, `foo.revert_to(4.days.ago)` only returns a __Fixnum__ `3`. Either way, I just want to return a `Foo` populated with the data that at a specified datetime. I feel like I'm overlooking something simple...
macek
I guess this is the longest thread!. I forgot the fact that revert_to returns the version number after modifying the underlying model. I have updated the code. This time it will work.
KandadaBoggu
Longest thread indeed! This finally worked! I had to do a little testing to figure out why the `and` syntax worked in your solution. For those following, `foo ||= 1 and 2` will assign `2` to `foo`. In the example above, `user.address.revert_to(updated_at)` sets `user.address` to the proper `Address` record, but the method returns a `Fixnum` of the **version number**, not the `Address` record. In order to assign the `Address` record to `@address`, the additional `... and user.address` is appended. Thanks again, KandadaBoggu <3
macek