<<

NAME

MT::Object - Movable Type base class for database-backed objects

SYNOPSIS

Creating an MT::Object subclass:

    package MT::Foo;
    use strict;

    use base 'MT::Object';

    __PACKAGE__->install_properties({
        columns_defs => {
            'id'  => 'integer not null auto_increment',
            'foo' => 'string(255)',
        },
        indexes => {
            foo => 1,
        },
        primary_key => 'id',
        datasource => 'foo',
    });

Using an MT::Object subclass:

    use MT;
    use MT::Foo;

    ## Create an MT object to load the system configuration and
    ## initialize an object driver.
    my $mt = MT->new;

    ## Create an MT::Foo object, fill it with data, and save it;
    ## the object is saved using the object driver initialized above.
    my $foo = MT::Foo->new;
    $foo->foo('bar');
    $foo->save
        or die $foo->errstr;

DESCRIPTION

MT::Object is the base class for all Movable Type objects that will be serialized/stored to some location for later retrieval; this location could be a DBM file, a relational database, etc.

Movable Type objects know nothing about how they are stored--they know only of what types of data they consist, the names of those types of data (their columns), etc. The actual storage mechanism is in the MT::ObjectDriver::Driver::DBI class and its driver subclasses; MT::Object subclasses, on the other hand, are essentially just standard in-memory Perl objects, but with a little extra self-knowledge.

This distinction between storage and in-memory representation allows objects to be serialized to disk in many different ways--for example, an object could be stored in a MySQL database, in a DBM file, etc. Adding a new storage method is as simple as writing an object driver--a non-trivial task, to be sure, but one that will not require touching any other Movable Type code.

SUBCLASSING

Creating a subclass of MT::Object is very simple; you simply need to define the properties and metadata about the object you are creating. Start by declaring your class, and inheriting from MT::Object:

    package MT::Foo;
    use strict;

    use base 'MT::Object';
  • __PACKAGE__->install_properties($args)

    Then call the install_properties method on your class name; an easy way to get your class name is to use the special __PACKAGE__ variable:

        __PACKAGE__->install_properties({
            column_defs => {
                'id' => 'integer not null auto_increment',
                'foo' => 'string(255)',
            },
            indexes => {
                foo => 1,
            },
            primary_key => 'id',
            datasource => 'foo',
        });

    install_properties performs the necessary magic to install the metadata about your new class in the MT system. The method takes one argument, a hash reference containing the metadata about your class. That hash reference can have the following keys:

    • column_defs

      The definition of the columns (fields) in your object. Column names are also used for method names for your object, so your column name should not contain any strange characters. (It could also be used as part of the name of the column in a relational database table, so that is another reason to keep column names somewhat sane.)

      The value for the columns key should be a reference to an hashref containing the key/value pairs that are names of your columns matched with their schema definition.

      The type declaration of a column is pseudo-SQL. The data types loosely match SQL types, but are vendor-neutral, and each MT::ObjectDriver will map these to appropriate types for the database it services. The format of a column type is as follows:

          'column_name' => 'type(size) options'

      The 'type' part of the declaration can be any one of:

      • string

        For storing string data, typically up to 255 characters, but assigned a length identified by '(size)'.

      • integer

        For storing integers, maybe limited to 32 bits.

      • boolean

        For storing boolean values (numeric values of 1 or 0).

      • smallint

        For storing small integers, typically limited to 16 bits.

      • datetime

        For storing a full date and time value.

      • timestamp

        For storing a date and time that automatically updates upon save.

      • blob

        For storing binary data.

      • text

        For storing text data.

      • float

        For storing floating point values.

      Note: The physical data storage capacity of these types will vary depending on the driver's implementation. Please refer to the documentation of the MT::ObjectDriver you're using to determine the actual capacity for these types.

      The '(size)' element of the declaration is only valid for the 'string' type.

      The 'options' element of the declaration is not required, but is used to specify additional attributes of the column. Such as:

      • not null

        Specify this option when you wish to constrain the column so that it must contain a defined value. This is only enforced by the database itself, not by the MT::ObjectDriver.

      • auto_increment

        Specify for integer columns (typically the primary key) to automatically assign a value.

      • primary key

        Specify for identifying the column as the primary key (only valid for a single column).

    • indexes

      Specifies the column indexes on your objects; this only has consequence for some object drivers (DBM, for example), where indexes are not automatically maintained by the datastore (as they are in a relational database).

      The value for the indexes key should be a reference to a hash containing column names as keys, and the value 1 for each key--each key represents a column that should be indexed.

      NOTE: with the DBM driver, if you do not set up an index on a column you will not be able to select objects with values matching that column using the load and load_iter interfaces (see below).

    • audit

      Automatically adds bookkeeping capabilities to your class--each object will take on four new columns: created_on, created_by, modified_on, and modified_by. The created_on, created_by columns will be populated automatically (if they have not already been assigned at the time of saving the object). Your application is responsible for updating the modified_on, modified_by columns as these may require explicit application-specific assignments (ie, your application may only want them updated during explicit user interaction with the object, as opposed to cases where the object is being changed and saved for mechanical purposes like upgrading a table).

    • datasource

      The name of the datasource for your class. The datasource is a name uniquely identifying your class--it is used by the object drivers to construct table names, file names, etc. So it should not be specific to any one driver.

    • meta

      Specify this property if you wish to add an additional 'meta' column to the object properties. This is a special type of column that is used to store complex data structures for the object. The data is serialized into a blob for storage using the MT::Serialize package.

    • meta_column

      If you wish to specify the name of the column to be used for storing the object metadata, you may declare this property to name the column. The default column name is 'meta'.

    • class_type

      If class_type is declared, an additional 'class' column is added to the object properties. This column is then used to differentiate between multiple object types that share the same physical table.

      Note that if this is used, all searches will be constrained to match the class type of the package.

    • class_column

      Defines the name of the class column (default is 'class') for storing classed objects (see 'class_type' above).

USAGE

System Initialization

Before using (loading, saving, removing) an MT::Object class and its objects, you must always initialize the Movable Type system. This is done with the following lines of code:

    use MT;
    my $mt = MT->new;

Constructing a new MT objects loads the system configuration from the mt.cfg configuration file, then initializes the object driver that will be used to manage serialized objects.

Creating a new object

To create a new object of an MT::Object class, use the new method:

    my $foo = MT::Foo->new;

new takes no arguments, and simply initializes a new in-memory object. In fact, you need not ever save this object to disk; it can be used as a purely in-memory object.

Setting and retrieving column values

To set the column value of an object, use the name of the column as a method name, and pass in the value for the column:

    $foo->foo('bar');

The return value of the above call will be bar, the value to which you have set the column.

To retrieve the existing value of a column, call the same method, but without an argument:

    $foo->foo

This returns the value of the foo column from the $foo object.

  • $obj->init()

This method is used to initialize the object upon construction.

  • $obj->set_defaults()

This method is used by the init method to set the object defaults.

Saving an object

To save an object using the object driver, call the save method:

  • $foo->save();

On success, save will return some true value; on failure, it will return undef, and you can retrieve the error message by calling the errstr method on the object:

    $foo->save
        or die "Saving foo failed: ", $foo->errstr;

If you are saving objects in a loop, take a look at the "Note on object locking".

Loading an existing object or objects

  • $obj->load()
  • $obj->load_iter()

You can load an object from the datastore using the load method. load is by far the most complicated method, because there are many different ways to load an object: by ID, by column value, by using a join with another type of object, etc.

In addition, you can load objects either into an array (load), or by using an iterator to step through the objects (load_iter).

load has the following general form:

    my @objects = MT::Foo->load(\%terms, \%arguments);

load_iter has the following general form:

    my $iter = MT::Foo->load_iter(\%terms, \%arguments);

Both methods share the same parameters; the only difference is the manner in which they return the matching objects.

If you call load in scalar context, only the first row of the array @objects will be returned; this works well when you know that your load call can only ever result in one object returned--for example, when you load an object by ID.

\%terms should be either:

  • The numeric ID of an object in the datastore.
  • A reference to a hash.

    The hash should have keys matching column names and the values are the values for that column.

    For example, to load an MT::Foo object where the foo column is equal to bar, you could do this:

        my @foo = MT::Foo->load({ foo => 'bar' });

    In addition to a simple scalar, the hash value can be a reference to an array; combined with the range setting in the \%arguments list, you can use this to perform range searches. If the value is a reference, the first element in the array specifies the low end of the range, and the second element the high end.

\%arguments should be a reference to a hash containing parameters for the search. The following parameters are allowed:

  • sort => "column"

    Sort the resulting objects by the column column; column must be an indexed column (see "indexes", above).

  • direction => "ascend|descend"

    To be used together with sort; specifies the sort order (ascending or descending). The default is ascend.

  • limit => "N"

    Rather than loading all of the matching objects (the default), load only N objects.

  • offset => "M"

    To be used together with limit; rather than returning the first N matches (the default), return matches M through N + M.

  • start_val => "value"

    To be used together with limit and sort; rather than returning the first N matches, return the first N matches where column (the sort column) is greater than value.

  • range

    To be used together with an array reference as the value for a column in \%terms; specifies that the specific column should be searched for a range of values, rather than one specific value.

    The value of range should be a hash reference, where the keys are column names, and the values are all 1; each key specifies a column that should be interpreted as a range.

  • join

    Can be used to select a set of objects based on criteria, or sorted by criteria, from another set of objects. An example is selecting the N entries most recently commented-upon; the sorting is based on MT::Comment objects, but the objects returned are actually MT::Entry objects. Using join in this situation is faster than loading the most recent MT::Comment objects, then loading each of the MT::Entry objects individually.

    Note that join is not a normal SQL join, in that the objects returned are always of only one type--in the above example, the objects returned are only MT::Entry objects, and cannot include columns from MT::Comment objects.

    join has the following general syntax:

        join => MT::Foo->join_on( JOIN_COLUMN, I<\%terms>, I<\%arguments> )

    Use the actual MT::Object-descended package name and the join_on static method providing these parameters: JOIN_COLUMN is the column joining the two object tables, \%terms and \%arguments have the same meaning as they do in the outer load or load_iter argument lists: they are used to select the objects with which the join is performed.

    For example, to select the last 10 most recently commmented-upon entries, you could use the following statement:

        my @entries = MT::Entry->load(undef, {
            'join' => MT::Comment->join_on( 'entry_id',
                        { blog_id => $blog_id },
                        { 'sort' => 'created_on',
                          direction => 'descend',
                          unique => 1,
                          limit => 10 } )
        });

    In this statement, the unique setting ensures that the MT::Entry objects returned are unique; if this flag were not given, two copies of the same MT::Entry could be returned, if two comments were made on the same entry.

  • unique

    Ensures that the objects being returned are unique.

    This is really only useful when used within a join, because when loading data out of a single object datastore, the objects are always going to be unique.

Removing an object

  • $foo->remove()

To remove an object from the datastore, call the remove method on an object that you have already loaded using load:

    $foo->remove();

On success, remove will return some true value; on failure, it will return undef, and you can retrieve the error message by calling the errstr method on the object:

    $foo->remove
        or die "Removing foo failed: ", $foo->errstr;

If you are removing objects in a loop, take a look at the "Note on object locking".

Removing select objects of a particular class

Combining the syntax of the load and remove methods, you can use the static version of the remove method to remove particular objects:

    MT::Foo->remove({ bar => 'baz' });

The terms you specify to remove by should be indexed columns. This method will load the object and remove it, firing the callback operations associated with those operations.

Removing all of the objects of a particular class

To quickly remove all of the objects of a particular class, call the remove_all method on the class name in question:

  • MT::Foo->remove_all();

On success, remove_all will return some true value; on failure, it will return undef, and you can retrieve the error message by calling the errstr method on the class name:

    MT::Foo->remove_all
        or die "Removing all foo objects failed: ", MT::Foo->errstr;

Removing all the children of an object

  • $obj->remove_children([ \%param ])

If your class has registered 'child_classes' as part of it's properties, then this method may be used to remove objects that are associated with the active object.

This method is typically used in an overridden 'remove' method.

    sub remove {
        my $obj = shift;
        $obj->remove_children({ key => 'object_id' });
        $obj->SUPER::remove(@_);
    }

The 'key' parameter specified here lets you identify the field name used by the children classes to relate back to the parent class. If unspecified, remove_children will assume the key to be the datasource name of the current class with an '_id' suffix.

Getting the count of a number of objects

To determine how many objects meeting a particular set of conditions exist, use the count method:

    my $count = MT::Foo->count({ foo => 'bar' });

count takes the same arguments (\%terms and \%arguments) as load and load_iter, above.

Determining if an object exists in the datastore

To check an object for existence in the datastore, use the exists method:

    if ($foo->exists) {
        print "Foo $foo already exists!";
    }

Counting groups of objects

  • $obj->count_group_by()

The count_group_by method can be used to retrieve a list of all the distinct values that appear in a given column along with a count of how many objects carry that value. The routine can also be used with more than one column, in which case it retrieves the distinct pairs (or n-tuples) of values in those columns, along with the counts. Yet more powerful, any SQL expression can be used in place of the column names to count how many object produce any given result values when run through those expressions.

  $iter = MT::Foo->count_group_by($terms, {%args, group => $group_exprs});

$terms and %args pick out a subset of the MT::Foo objects in the usual way. $group_expressions is an array reference containing the SQL expressions for the values you want to group by. A single row will be returned for each distinct tuple of values resulting from the $group_expressions. For example, if $group_expressions were just a single column (e.g. group => ['created_on']) then a single row would be returned for each distinct value of the 'created_on' column. If $group_expressions were multiple columns, a row would be returned for each distinct pair (or n-tuple) of values found in those columns.

Each application of the iterator $iter returns a list in the form:

  ($count, $group_val1, $group_val2, ...)

Where $count is the number of MT::Foo objects for which the group expressions are the values ($group_val1, $group_val2, ...). These values are in the same order as the corresponding group expressions in the $group_exprs argument.

In this example, we load up groups of MT::Pip objects, grouped by the pair (cat_id, invoice_id), and print how many pips have that pair of values.

    $iter = MT::Pip->count_group_by(undef,
                                    {group => ['cat_id',
                                               'invoice_id']});
    while (($count, $cat, $inv) = $iter->()) {
        print "There are $count Pips with " .
            "category $cat and invoice $inv\n";
    }

Inspecting and Manipulating Object State

  • $obj->column_values()

Use column_values and set_values to get and set the fields of an object en masse. The former returns a hash reference mapping column names to their values in this object. For example:

    $values = $obj->column_values()
  • $obj->set_values()

set_values accepts a similar hash ref, which need not give a value for every field. For example:

    $obj->set_values({col1 => $val1, col2 => $val2});

is equivalent to

    $obj->col1($val1);
    $obj->col2($val2);

Other Methods

  • $obj->clone([\%param])

    Returns a clone of $obj. That is, a distinct object which has all the same data stored within it. Changing values within one object does not modify the other.

    An optional except parameter may be provided to exclude particular columns from the cloning operation. For example, the following would clone the elements of the blog except the name attribute.

       $blog->clone({ except => { name => 1 } });
  • $obj->column_names()

    Returns a list of the names of columns in $obj; includes all those specified to the install_properties method as well as the audit properties (created_on, modified_on, created_by, modified_by), if those were enabled in install_properties.

  • $obj->set_driver()

    This method sets the object driver to use to link with a database.

  • MT::Foo->driver()
  • $obj->driver()

    Returns the ObjectDriver object that links this object with a database.

  • $obj->created_on_obj()

    Returns an MT::DateTime object representing the moment when the object was first saved to the database.

  • MT::Foo->set_by_key($key_terms, $value_terms)

    A convenience method that loads whatever object matches the $key_terms argument and sets some or all of its fields according to the $value_terms. For example:

       MT::Foo->set_by_key({name => 'Thor'},
                           {region => 'Norway', gender => 'Male'});

    This loads the MT::Foo object having 'name' field equal to 'Thor' and sets the 'region' and 'gender' fields appropriately.

    More than one term is acceptable in the $key_terms argument. The matching object is the one that matches all of the $key_terms.

    This method only useful if you know that there is a unique object matching the given key. There need not be a unique constraint on the columns named in the $key_hash; but if not, you should be confident that only one object will match the key.

  • MT::Foo->get_by_key($key_terms)

    A convenience method that loads whatever object matches the $key_terms argument. If no matching object is found, a new object will be constructed and the $key_terms provided will be assigned to it. So regardless of whether the key exists already, this method will return an object with the key requested. Note, however: if a new object is instantiated it is not automatically saved.

        my $thor = MT::Foo->get_by_key({name => 'Thor'});
        $thor->region('Norway');
        $thor->gender('Male');
        $thor->save;

    The fact that it returns a new object if one isn't found is to help optimize this pattern:

        my $obj = MT::Foo->load({key => $value});
        if (!$obj) {
            $obj = new MT::Foo;
            $obj->key($value);
        }

    This is equivalent to:

        my $obj = MT::Foo->get_by_key({key => $value});

    If you don't appreciate the autoinstantiation behavior of this method, just use the load method instead.

    More than one term is acceptable in the $key_terms argument. The matching object is the one that matches all of the $key_terms.

    This method only useful if you know that there is a unique object matching the given key. There need not be a unique constraint on the columns named in the $key_hash; but if not, you should be confident that only one object will match the key.

  • $obj->cache_property($key, $code)

    Caches the provided key (e.g. entry, trackback) with the return value of the given code reference (which is often an object load call) so that the value does not have to be recomputed each time.

  • $obj->column_def($name)

    This method returns the value of the given $name column_defs propery.

  • $obj->column_defs()

    This method returns all the column_defs of the property of the object.

  • $obj->to_hash()

    TODO - So far I have not divined what this method actually does. Hints?

  • Class->join_on()

    This method returns the list of used by the join arguments parameter used by the "listing" in MT::App::CMS method.

  • $obj->properties()

    TODO - Return the return properties of the object.

  • $obj->to_xml()

    TODO - Returns the XML representation of the object. This method is defined in MT/BackupRestore.pm - you must first use MT::BackupRestore to use this method.

  • $obj->restore_parent_ids()

    TODO - Backup file contains parent objects' ids (foreign keys). However, when parent objcects are restored, their ids will be changed. This method is to match the old and new ids of parent objects for children objects to be correctly associated. This method is defined in MT/BackupRestore.pm - you must first use MT::BackupRestore to use this method.

  • $obj->parent_names()

    TODO - Should be overridden by subclasses to return correct hash whose keys are xml element names of the object's parent objects and values are class names of them. This method is defined in MT/BackupRestore.pm - you must first use MT::BackupRestore to use this method.

NOTES

Note on object locking

When you read objects from the datastore, the object table is locked with a shared lock; when you write to the datastore, the table is locked with an exclusive lock.

Thus, note that saving or removing objects in the same loop where you are loading them from an iterator will not work--the reason is that the datastore maintains a shared lock on the object table while objects are being loaded from the iterator, and thus the attempt to gain an exclusive lock when saving or removing an object will cause deadlock.

For example, you cannot do the following:

    my $iter = MT::Foo->load_iter({ foo => 'bar' });
    while (my $foo = $iter->()) {
        $foo->remove;
    }

Instead you should do either this:

    my @foo = MT::Foo->load({ foo => 'bar' });
    for my $foo (@foo) {
        $foo->remove;
    }

or this:

    my $iter = MT::Foo->load_iter({ foo => 'bar' });
    my @to_remove;
    while (my $foo = $iter->()) {
        push @to_remove, $foo
            if SOME CONDITION;
    }
    for my $foo (@to_remove) {
        $foo->remove;
    }

This last example is useful if you will not be removing every MT::Foo object where foo equals bar, because it saves memory--only the MT::Foo objects that you will be deleting are kept in memory at the same time.

CALLBACKS

  • $obj->add_callback()

Most MT::Object operations can trigger callbacks to plugin code. Some notable uses of this feature are: to be notified when a database record is modified, or to pre- or post-process the data being flowing to the database.

To add a callback, invoke the add_callback method of the MT::Object subclass, as follows:

   MT::Foo->add_callback( "pre_save", <priority>, 
                          <plugin object>, \&callback_function);

The first argument is the name of the hook point. Any MT::Object subclass has a pre_ and a post_ hook point for each of the following operations:

    load
    save
    remove
    remove_all
    (load_iter operations will call the load callbacks)

The second argument, <priority>, is the relative order in which the callback should be called. The value should be between 1 and 10, inclusive. Callbacks with priority 1 will be called before those with 2, 2 before 3, and so on.

Plugins which know they need to run first or last can use the priority values 0 and 11. A callback with priority 0 will run before all others, and if two callbacks try to use that value, an error will result. Likewise priority 11 is exclusive, and runs last.

How to remember which callback priorities are special? As you know, most guitar amps have a volume knob that goes from 1 to 10. But, like that of certain rock stars, our amp goes up to 11. A callback with priority 11 is the "loudest" or most powerful callback, as it will be called just before the object is saved to the database (in the case of a 'pre' callback), or just before the object is returned (in the case of a 'post' callback). A callback with priority 0 is the "quietest" callback, as following callbacks can completely overwhelm it. This may be a good choice for your plugin, as you may want your plugin to work well with other plugins. Determining the correct priority is a matter of thinking about your plugin in relation to others, and adjusting the priority based on experience so that users get the best use out of the plugin.

The <plugin object> is an object of type MT::Plugin which gives some information about the plugin. This is used to include the plugin's name in any error messages.

<callback function> is a code referense for a subroutine that will be called. The arguments to this function vary by operation (see MT::Callback for details), but in each case the first parameter is the MT::Callback object itself:

  sub my_callback {
      my ($cb, ...) = @_;

      if ( <error condition> ) {
          return $cb->error("Error message");
      }
  }

Strictly speaking, the return value of a callback is ignored. Calling the error() method of the MT::Callback object ($cb in this case) propagates the error message up to the MT activity log.

Another way to handle errors is to call die. If a callback dies, MT will warn the error to the activity log, but will continue processing the MT::Object operation: so other callbacks will still run, and the database operation should still occur.

Any-class Object Callbacks

If you add a callback to the MT class with a hook point that begins with *::, such as:

    MT->add_callback('*::post_save', 7, $my_plugin, \&code_ref);

then it will be called whenever post_save callbacks are called. "Any-class" callbacks are called after all class-specific callbacks. Note that add_callback must be called on the MT class, not on a subclass of MT::Object.

  • $obj->set_callback_routine()

    This method just calls the set_callback_routine as defined by the MT::ObjectDriver set with the set_driver method.

Caveat

Be careful how you handle errors. If you transform data as it goes into and out of the database, and it is possible for one of your callbacks to fail, the data may get saved in an undefined state. It may then be difficult or impossible for the user to recover that data.

AUTHOR & COPYRIGHTS

Please see the MT manpage for author, copyright, and license information.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 906:

'=item' outside of any '=over'

Around line 1078:

You forgot a '=back' before '=head1'

<<