Class::ReluctantORM::Audited - CRO clases with Audit Logging
package ImportantThing; use strict; use warnings; use base 'Class::ReluctantORM::Audited'; __PACKAGE__->build_class( # Normal Class::ReluctantORM->build_class options table => 'important_things', schema => 'stuff', primary_key => 'thing_id', ... # Additional audit table options audit_table_name => 'important_changes', audit_schema_name => 'audit', audit_primary_key => 'change_id', # Or array ref audit_columns => [qw(audit_user_id audit_ip_address)], audit_insert => 0, # also delete, update audit_seamless_mode => 1, # hijack old method names, make purists cry ); # Two ways to provide audit metadata: $thing->audited_update($user_id, $ip_address); # or magically, if you define # ImportantThing::get_audit_metadata_audit_user_id and # ImportantThing::get_audit_metadata_audit_ip_address $thing->audited_update(); # To make update() explode, use audit_seamless_mode => 0 (default) # this still works because of audit_seamless_mode => 1 # but you must have the get_audit_data methods $thing->update();
Many times it is neccesary to log changes to tables. This log, called an audit log, typically retains the previous state of the row, along with the action that was performed, and any additional metadata deemed needed by the business (such as who made the change). This class exists to make such work easier.
This is a Class::ReluctantORM subclass. All its methods are available here. save(), update(), insert(), create(), and delete() are re-defined in this class.
This class expects there to be a separate table for logging changes made against each orginal table. It may be in any schema, and have any name. No CRO triggers, DB triggers or stored procedures are used.
To create your audit table, create a new table with the same column and same datatypes as the original table, with the following modifications:
The audit metadata must be provided "out of band" - it is not part of the actual update/insert/delete, but must be provided for auditing. To support this, two approaches are available.
You may provide the metadata directly when calling audited_update, audited_delete, audited_insert, or audited_save. (audited_create is handled specially). You must provide all values, and you must provide them in the order specified in build_class.
$thing->audited_update($changing_user_id, $ip_address);
For audited_create, you may provide the audit metadata as additional columns. Your audit column names must be distinct from your original column names for this to work.
ImportantThing->audited_create( name => 'whatever', other_field => $stuff, ... audit_user_id => $changing_user_id, audit_ip_address => $ip_address, );
The downside of this approach is that your calling code must provide these values every time it performs an audited operation, which becomes tiresome.
Alternatively, you can provide instance methods in your subclass that will obtain each piece of metadata when performing an audited action. The methods should have names that begin with 'get_audit_metadata_FIELDNAME':
sub get_audit_metadata_audit_user_id { my $self = shift; # magically determine $changing_user_id from globals or $self return $changing_user_id; }
With that (and any other generators in place) you can now simply call:
$thing->audited_update();
You may be wondering what happens now when someone calls, for example, $thing->update(). You have your choice of poisons.
Calling the plain version of any audited action will throw an exception. You are required to call the audited_whatever version.
This is more more "pure", in terms of software design. By changing the preconditions for calling a method, we have violated a contract, and we should let the user know it's no longer safe to call it the old way.
The downside is that you now have to retrofit all old code to call audited_whatever. You'll also need to arrange to have the audit metadata available at each of these invocations.
Calling the plain version of any audited action will attempt to obtain the audit metadata, then hand off to the audited version.
This means you can do this:
$thing->update($changing_user_id, $ip_address); # Or if youve defined metatdata generator methods, simply $thing->update();
That's a lot of magic going on there. This may lead to unexpected suprises (especially if a metatdata generator fails), but the upside is that you might not have to change any code.
Sets up your class for auditing. Accepts all options that the stock Class::ReluctantORM build_class accepts (in any order). In addition, the following auditing options are supported:
Required. Name of the schema in the database for the audit table.
Required. Name of audit table in the database. For details on how this table should be constructed, see CREATING AN AUDIT TABLE.
Optional. String, or arrayref of strings, identifying an optional primary key on the log table itself. This allows you to call last_audit_primary_key_value() on your original object, and thus identify individual log entries.
Optional arrayref of strings. These are extra columns (not present in the original table) that you would also like to include in the log (for example, IP address). For details on how to pass these values, see PROVIDING AUDIT COLUMN VALUES.
Optional booleans, default true. You may set any of these to 0 to disable auditing on that action. Disabling all of them disables auditing.
Boolean, default false. If you provide a true value, seamless mode is enabled. See SEAMLESS MODE above.
Always returns true. Plain Class::ReluctantORM classes always return false.
Returns an array of column names that are treated as audit metadata columns. This array is obtained directly from build_class().
If your audit table has a primary key (for change tracking, etc), this will return the column names that make up the primary key.
Returns an empty list if you did not pass a value for audit_primary_key to build_class.
Returns true if seamless mode is enabled for this class.
Returns true if inserts (and creates) are being audited.
Returns true if updates are being audited.
Returns true if deletes are being audited.
Returns the name of the schema that the audit table lives in.
Returns the name of the audit table in the database.
If your audit table has a primary key, this will return the primary value of the last log entry.
If your audit table has a single-column primary key, the one value will be returned.
If your audit table has a multi-column primary key, an arrayref of the values, in the same order you used in build_class, will be returned.
In list context, an array is alwas returned.
If no audited action has occurred, it will return undef or an empty list.
For each of these methods, the arguments are optional if you have defined metadata generator methods.
Copies the existing row in the database to the audit table along with the metadata, then performs the update on the original table.
Performs the insert on the original table, then copies the new primary key and the metadata to the audit table (all other columns are left NULL, as this is a new row and the audit table only records past states).
Copies the existing data from the original table along with the metadata to the audit table, then performs the delete on the original table.
Performs either an audited insert or an audited_update, depending on whether the object has been saved to the database.
NOTE: This does NOT call the before_save_trigger nor the after_save_trigger, due to a sequencing conflict. We're looking for ways around that, but keep in mind the insert and update triggers will be called normally.
Like Class::ReluctantORM's create(), creates a new object in memory and immediately commits it to the database. Here, you may also specify the audit metadata as well, which will get separated out.
Clinton Wolfe clinton@omniti.com
To install Class::ReluctantORM, copy and paste the appropriate command in to your terminal.
cpanm
cpanm Class::ReluctantORM
CPAN shell
perl -MCPAN -e shell install Class::ReluctantORM
For more information on module installation, please visit the detailed CPAN module installation guide.