2

When the staff change the information of product name, option name or prices. It should insert the data into history log and who done it.

items table:

item_id (PK)
item_name
item_description

Note: item prices are in the item_options table

item_options table:

option_id (PK)
item_id (FK)
option_name
option_price

A item can have 1 or more options.

If I want to change the name items.item_name, It should copy the current record to the history table, delete current record from items table and then insert a new record with the new information in the items table?

What about the item_options, how would that work? If there are multiple options from specific item_id, do that mean I need to duplicate options to history table?

What Audit logging/history tables should look like for items and item_options?

Thanks

user622378
  • 2,318
  • 6
  • 42
  • 63

2 Answers2

5

Your audit data should be stored per-table, rather than all in one place. What you'd do is create an audit table for each of the tables you want to track, and create triggers to create a record in the audit table for any data-manipulation operation on the audited table.

It's definitely advisable to disallow DELETE operations on the items and item_options tables - add flags like item_active and item_option_active so that you can softdelete them instead. This is normal practice in situations where you're doing things like storing invoices that reference products ordered in the past, and need the data for historical reporting purposes, but not for day-to-day use.

Your audit tables aren't something you should use for referencing old data, your normal data model should support simply "hiding" old data where it's likely that it's still going to be used, and storing multiple versions of data that will change over time.

For auditing, it's also useful to store the username of the last user to modify a given record - when used from a web application, you can't use MySQL's USER() function to get any useful information about who's logged on. Adding a column and populating it means you can use that information in your audit triggers.

NB: I'll assume that you won't allow item IDs to be changed under normal conditions - that would make your auditing system more complex.

If you add active flags, and last-modified-by data to your tables, they'll look something like:

Items table:

mysql> desc items;
+------------------+--------------+------+-----+---------+----------------+
| Field            | Type         | Null | Key | Default | Extra          |
+------------------+--------------+------+-----+---------+----------------+
| item_id          | int(11)      | NO   | PRI | NULL    | auto_increment |
| item_name        | varchar(100) | YES  |     | NULL    |                |
| item_description | text         | YES  |     | NULL    |                |
| item_active      | tinyint(4)   | YES  |     | NULL    |                |
| modified_by      | varchar(50)  | YES  |     | NULL    |                |
+------------------+--------------+------+-----+---------+----------------+

Item options table:

mysql> desc item_options;
+---------------+--------------+------+-----+---------+----------------+
| Field         | Type         | Null | Key | Default | Extra          |
+---------------+--------------+------+-----+---------+----------------+
| option_id     | int(11)      | NO   | PRI | NULL    | auto_increment |
| item_id       | int(11)      | YES  | MUL | NULL    |                |
| option_name   | varchar(100) | YES  |     | NULL    |                |
| option_price  | int(11)      | YES  |     | NULL    |                |
| option_active | tinyint(4)   | YES  |     | NULL    |                |
| modified_by   | varchar(50)  | YES  |     | NULL    |                |
+---------------+--------------+------+-----+---------+----------------+

Your audit tables need to store four extra pieces of information:

  • Audit ID - this ID is only unique for the history of this table, it's not a global value
  • Change made by - the database user who made the change
  • Change date/time
  • Action type - INSERT or UPDATE (or DELETE if you were allowing it)

Your audit tables should look something like:

Items audit table:

mysql> desc items_audit;
+------------------+--------------+------+-----+---------+----------------+
| Field            | Type         | Null | Key | Default | Extra          |
+------------------+--------------+------+-----+---------+----------------+
| audit_id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| item_id          | int(11)      | YES  |     | NULL    |                |
| item_name        | varchar(100) | YES  |     | NULL    |                |
| item_description | text         | YES  |     | NULL    |                |
| item_active      | tinyint(4)   | YES  |     | NULL    |                |
| modified_by      | varchar(50)  | YES  |     | NULL    |                |
| change_by        | varchar(50)  | YES  |     | NULL    |                |
| change_date      | datetime     | YES  |     | NULL    |                |
| action           | varchar(10)  | YES  |     | NULL    |                |
+------------------+--------------+------+-----+---------+----------------+

Item options audit table:

mysql> desc item_options_audit;
+---------------+--------------+------+-----+---------+----------------+
| Field         | Type         | Null | Key | Default | Extra          |
+---------------+--------------+------+-----+---------+----------------+
| audit_id      | int(11)      | NO   | PRI | NULL    | auto_increment |
| option_id     | int(11)      | YES  |     | NULL    |                |
| item_id       | int(11)      | YES  |     | NULL    |                |
| option_name   | varchar(100) | YES  |     | NULL    |                |
| option_price  | int(11)      | YES  |     | NULL    |                |
| option_active | tinyint(4)   | YES  |     | NULL    |                |
| modified_by   | varchar(50)  | YES  |     | NULL    |                |
| change_by     | varchar(50)  | YES  |     | NULL    |                |
| change_date   | datetime     | YES  |     | NULL    |                |
| action        | varchar(10)  | YES  |     | NULL    |                |
+---------------+--------------+------+-----+---------+----------------+

Don't use foreign keys on your audit tables; the rows in the audit tables aren't child rows of the records they're auditing, so foreign keys aren't of any use.

Triggers

NB: MySQL doesn't support multi-statement-type triggers, so you need one for each of INSERT, UPDATE and DELETE (if applicable).

Your triggers simply need to INSERT all the NEW values into the audit table. The trigger definitions for the items table might be:

/* Trigger for INSERT statements on the items table */
CREATE DEFINER=`root`@`localhost` TRIGGER trigger_items_insert_audit 
AFTER INSERT ON items 
  FOR EACH ROW BEGIN
    INSERT INTO items_audit (
                  item_id, item_name, item_description, 
                  item_active, modified_by, change_by,  
                  change_date, action
                ) VALUES (
                  NEW.item_id, NEW.item_name, NEW.item_description,  
                  NEW.item_active, NEW.modified_by, USER(),  
                  NOW(), 'INSERT'
                ); 
  END;

/* Trigger for UPDATE statements on the items table */
CREATE DEFINER=`root`@`localhost` TRIGGER trigger_items_update_audit 
AFTER UPDATE ON items 
  FOR EACH ROW BEGIN
    INSERT INTO items_audit (
                  item_id, item_name, item_description, 
                  item_active, modified_by, change_by,  
                  change_date, action
                ) VALUES (
                  NEW.item_id, NEW.item_name, NEW.item_description,  
                  NEW.item_active, NEW.modified_by, USER(),  
                  NOW(), 'UPDATE'
                ); 
  END;

Create similar triggers for the item_options table.

Update: Data History In E-commerce

The auditing we did above will allow you to keep a history of any given database table, but creates a data store that isn't suitable for use for data that needs to be accessed regularly.

In an e-commerce system, keeping usable historical data is important, so that you can change attributes while still presenting old values in certain situations.

This should be completely separate from your auditing solution

The best way to store history is to create a history table for each attribute that needs to be stored historically. This Stackoverflow question has some good information about keeping a history of a given attribute.

In your situation, if you're only concerned about price and title, you'd create a prices table, and an item_titles table. Each one would have a foreign key to either the item_options table or the items table (the master tables would still store the current price, or title), and would have the price or title, with its effective dates. These tables should have fine-grained (possibly column-based) permissions to avoid updating the effective_from dates, and the actual values once the record is inserted.

You should use the auditing solution above on these tables also.

Community
  • 1
  • 1
Simon
  • 1,980
  • 14
  • 21
  • In regarding to the soft delete, Let say I change the price in item_options.option_name - I would simply item_option_active to NO and insert a new row with item_options table? What about if I change the item name (items.item_name)? Do that mean I have to deactive all the options (item_options.item_id) as well? and then duplicate new row options with the new item_id key? – user622378 Apr 25 '11 at 09:01
  • You say there is no need to recreate records in the items and items_options tables but if I update the prices and item name - the past orders of invoices will be changed and the prices will be incorrect.. so what is the solution to this? – user622378 Apr 25 '11 at 10:02
  • My answer is a little incomplete, (and I did in fact miss quite an important link from the end, I'm updating it now) it's a case of "-1, wrote answer while tired". I concentrated entirely on generic structures for "how to audit a database table" and neglected to mention considerations for e-commerce sites. – Simon Apr 25 '11 at 11:58
  • I've added an extra section, covering historical data – Simon Apr 25 '11 at 12:20
  • Thank you, waiting for update regarding to invoices when the prices or item name changes :) – user622378 Apr 25 '11 at 12:21
  • The historical data allows that - you know what date an invoice was generated, and the history tables (*not* the audit tables) for price, title (whatever other factors you choose) allow you to look up if a different price for that item was effective at the date it was generated - i.e. does the invoice date fall into a range present in one of the tables with a foreign key to the item in question. – Simon Apr 25 '11 at 12:24
  • Thank you for update, that was helpful, Now I understoood but how would invoice know which one to look at.. item_options for current price or the old prices from prices table? Let say in the invoice table I have invoice.option_id = 3 ... then I have 5 rows with same id in prices table and item_options table – user622378 Apr 25 '11 at 12:35
  • 1
    Each row in `prices` will have `effective_from` and `effective_to`. Your invoice will have `date_created`. If `date_created` from the `invoice` table falls between the `effective_from` and `effective_to` dates from the `prices` table for one of the lines on the invoice, then you use *that* price rather than the current (or any other) – Simon Apr 25 '11 at 12:37
  • Thanks. So if a record NOT found between effective_from and effective_to dates from the prices then that mean get current price from items_option table because price have not been change? – user622378 Apr 25 '11 at 12:51
  • Yes. Remember that depending on how you implement this, the current price may also be in the `prices` table - without the `effective_to` date. You either insert prices into the `prices` table when they are created, or when they're changed. If you do the former, all current prices will be there with a `NULL` in the `effective_to` column. – Simon Apr 25 '11 at 12:55
  • Right now I have over 200,000 rows in the items_option with the price. I think im going to move all the current price to prices tables. I think that will be best solution to make SQL query simpler? Im not understanding what effective_to and effective_from mean and which should be updated when the price change. Don't mind editing your answer to provice example of that? thanks – user622378 Apr 25 '11 at 13:05
  • 1
    Did you look at the accepted answer for the [question I linked to](http://stackoverflow.com/questions/4823025/what-is-the-best-way-to-store-a-historical-price-list-in-a-mysql-table)? It explains how you implement the history. Each time the price changes, `prices` gets a new row inserted, and the old row (the one without an `effective_to` date) gets the `effective_to` date set to `NOW()`. – Simon Apr 25 '11 at 13:15
  • I understood now :) There is nothing wrong removing the item_options.price ? - When I add new product - the new price will be added to prices instead. – user622378 Apr 25 '11 at 13:22
  • `item_options.price` can be kept to make displaying "current data" easier (no need to join to `prices`), but if you prefer to remove it, that also makes sense - it's the more normalised form of the data. One other thing to consider is triggers to update the historical data, similar to how the audit triggers work. You could keep `item_options.price`, and whenever it changes, have a trigger sort out the `prices` table accordingly. – Simon Apr 25 '11 at 13:35
0

if you do not have a bunch of constraints - then your data will get messed up in a hurry when you orphan the item entries by removing option entries and visaversa.

what you are asking for can be done in triggers, but this is not probably what you want.

imaging if you have an item with 2 options.

now you change the item name, that item gets deelted (and moved to history) - you have unlinkable options... is that what you intend?

what about order or other things that reference the items? same issues.

instead, create trigger logic to only allow 'reasonable' edits to the item. if desired, put a copy of the record into a parallel history table, but DO NOT delete the original.

you may also consider adding a status column to the item or some date ranges in order to account for the idea that this item is currently available or whatever other status you may need.

Randy
  • 16,480
  • 1
  • 37
  • 55
  • You are right, it will be unlinkable options. I think it would be better to add a field item_active in the items table but what about the options. Should that be inactive as well when I change the items.item_name name? – user622378 Apr 25 '11 at 09:51