Manual:ORMTable

From MediaWiki.org
Jump to navigation Jump to search

This page documents the ORMTable class and associated ORMRow and ORMResult classes, which are available from MediaWiki core since version 1.20. It will be removed in 1.27.

Rationale[edit]

Often you have some type of object which you store in a single database table, each row corresponding to an instance of this object. These can be users, surveys, campaigns or any other type of entity. Doing the mapping between database records and such objects is not hard, but it contains a lot of boilerplate code, shared by all such objects. This leads to all these objects having their own interfaces, and developers being forced to bother writing these in the first place rather then the actual logic they care about. These issues only get worse for the kinds of objects doing more complex interaction with the database. Therefore it's nice to have an abstract base that takes care of all of this, and as a bonus, can take care of hiding some information about the database you probably don't want any other code to know about.

Implementation[edit]

The ORMTable approach abstracts interaction with a database table by having a class that represents the table (ORMTable) and one that represents rows in this table (ORMRow).

The table class holds information about the table such as its name, which fields it has and what the field prefix[1] is. All interaction with the table is done through an instance of this class[2], which can be obtained via its singleton method. The table class has utility methods for doing select, update, deletion and count operations, which behave like the usual DatabaseBase class methods, except that they do not require the table name and expect unprefixed field names. And in case of select methods, an iterator with row classes is returned rather then raw objects. The class also acts as a factory for rows. New rows can be constructed using $table->newRow. The table instance gets passed to each row it creates.

The row class acts as a simple wrapper for the rows data that also holds methods to inset, update and delete the object in the database. The field data it holds can be obtained via getField(s) and set via setField(s), both using unprefixed field names. Serialization of the fields is handled by the class based on the type of the field. So boolean fields will always have boolean values (rather then '0' or '1'), and array fields will always be arrays (rather then strings in some serialization format). The object also keeps track of the id of the row that represents it in the database, which is needed by update and delete. When the id is not set, the object will assume it has no corresponding row yet, which will cause save to insert the object. The wrapper functionality and query methods are all very basic, but since you can arbitrarily extend the row class with functionality specific to the type of object it represents, this really suffices.

Both the table and row classes are type hinted against using interfaces, IORMTable and IORMRow respectively.

Current implementation issues[edit]

The current implementation has some design issues one should be aware of in order to avoid problems.

  • IORMRow implementing objects take a IORMTable object in their constructor. The latter is a hard to construct service object while the former ideally should just be a simple wrapper around a database row. This means IORMRow objects are tightly coupled with IORMTable objects, which makes them inflexible and makes various things such as testing logic contained in them needlessly difficult.
  • IORMRow (and documentation on this page) encourages embedding of business logic into itself while it's a data object. Though this might work out in prototyping, mixing business logic in data objects is bad practice causing numerous problems.

Improvements that have been made and should still be made:

  • Deprecated sigleton in IORMTable and made ORMTable constructor public. This was done in 1.21.
  • Made ORMTable non-abstract so one does not have to subclass without good reason. This was done in 1.21.
  • IORMRow objects should not contain a IORMTable or know about this interface. Not done yet. In progress

ORMTable[edit]

The below list of methods is meant to give you an idea of how to use the class. For a complete overview with full documentation, see the source of the IORMTable interface.

Singleton[edit]

Access to a table is typically done via the singleton method.

Courses::singleton()->count();

// Or
$coursesTable = Courses::singleton();
$coursesTable->count();
$coursesTable->update( /* ... */ );
$coursesTable->delete( /* ... */ );

Abstract methods[edit]

These methods need to be implemented in every non-abstract deriving class.

getName[edit]

Returns the name of the database table objects of this type are stored in. This is the only place the table name needs to be specified (not counting joins).

Usage:

$tableName = $courseTable->getName();

Returns: string, the table name

getRowClass[edit]

Returns the name of a IORMRow implementing class that represents single rows in this table.

Usage:

$rowClass = $courseTable->getRowClass();

Returns: string, the row class name

getFieldPrefix[edit]

Returns the prefix of the tables fields used in the database. This information should typically not be used anywhere outside of the table class, hence this method is protected.

Usage:

$fieldPrefix = $courseTable->getFieldPrefix();

Returns: string, the field prefix

getFields[edit]

Returns an array with the fields and their types this object contains. This corresponds directly to the fields in the database, without prefix. The returned array contains the field names as keys pointing to their types. The types can be:

  • id
  • str
  • int
  • float
  • bool
  • array
  • blob

Usage:

$fields = $courseTable->getFields();

Returns: array

Commonly overridden methods[edit]

getDefaults[edit]

Returns a list of default field values. The returned array contains field names as keys pointing to their default value. By default the returned array is empty, but deriving classes can override it to provide defaults.

Usage:

$defaults = $courseTable->getDefaults();

Returns: array

Query methods[edit]

select[edit]

Selects the the specified fields for the matching records and returns them in an ORMResult, which is an iterator containing IORMRow implementing classes.

Important args:

  • $fields - the fields to select. Array, string or null. Null for all fields. Null is the default.
  • $conditions - conditions the select should match. Field name => value. Field name without prefix.

Selecting all courses:

$allCourses = $courseTable->select();

Selecting the id and name of all courses:

$allCourses = $courseTable->select( array( 'id', 'name' ) );

Selecting all courses with 42 students:

$awesomeCourses = $courseTable->select( null, array( 'student_count' => 42 ) );

update[edit]

Update the specified fields for the matching records.

Important args:

  • $values - Array. Field name => new value. Field name without prefix.
  • $conditions - conditions the select should match. Field name => value. Field name without prefix.

All courses with 42 students are awesome:

$courseTable->update(
    array( 'awesome' => true ),
    array( 'student_count' => 42 )
);

delete[edit]

Delete the matching records.

Important args:

  • $conditions - conditions the select should match. Field name => value. Field name without prefix.

Delete non-awesome courses:

$courseTable->delete( array( 'awesome' => false ) );

count[edit]

Count the matching records.

Important args:

  • $conditions - conditions the select should match. Field name => value. Field name without prefix.

Count the awesome courses:

$courseTable->count( array( 'awesome' => true ) );

has[edit]

See if there are any matching records.

Important args:

  • $conditions - conditions the select should match. Field name => value. Field name without prefix.

Are there any awesome courses with 42 students:

$courseTable->has( array( 'awesome' => true, 'student_count' => 42 ) );

If there are any courses:

$courseTable->has();

selectRow[edit]

Select only the first matching row and return it as IORMRow implementing object (or false if nothing matches).

Important args:

  • $fields - the fields to select. Array, string or null. Null for all fields. Null is the default.
  • $conditions - conditions the select should match. Field name => value. Field name without prefix.

Selecting the Master in Angry Birds course:

$myCourse = $courseTable->selectRow( null, array( 'name' => 'Master in Angry Birds' ) );

Selecting the Master in Angry Birds course and only loading the id field:

$myCourse = courseTable->selectRow( 'id', array( 'name' => 'Master in Angry Birds' ) );

selectFields[edit]

Selects the the specified fields of the records matching the provided conditions and returns them as associative arrays.

Important args:

  • $fields - the fields to select. Array, string or null. Null for all fields. Null is the default.
  • $conditions - conditions the select should match. Field name => value. Field name without prefix.
  • $collapse - true by default, and not the third arg

When $collapse is true:

  • If one field is selected, each item in the result array will be this field.
  • If two fields are selected, each item in the result array will have as key the first field and as value the second field.
  • If more then two fields are selected, each item will be an associative array.

Selecting the name and id of the awesome courses:

$courseTable->selectFields( array( 'id', 'name' ), array( 'awesome' => true ) );

Results into:

array(
    array( 4 => 'foo' ),
    array( 2 => 'bar' ),
    // ...
)

if $collapse was false, it would result into:

array(
    array( 'id' => 4, 'name' => 'foo' ),
    array( 'id' => 2, 'name' => 'bar' ),
    // ...
)

Selecting all terms (using the third arg $options):

$courseTable->selectFields( 'term', array(), array( 'DISTINCT' ) );

Results into:

array(
    'q1 2012',
    'q2 2012',
    // ...
)

Factory methods[edit]

These methods allow for creating new instances of IORMRow implementing classes via their corresponding IORMTable implementing class.

newRow[edit]

Constructs a new instance of your row class (the one specified in IORMTable::getRowClass) given an array.

Important args:

  • $data - the field values to instantiate the object with. Array. Field names pointing to values.
  • $loadDefaults - boolean indicating if the default values should be loaded for the fields that have no value specified in $data.
$newCourse = $courseTable->newRow( array( 'awesome' => true, 'language' => 'en' ) );
// After which you can for instance do $newCourse->save();

newRowFromDBResult[edit]

Constructs a new instance of your row class (the one specified in IORMTable::getRowClass) given a database result. This is used internally by ORMResult for constructing IORMRow implementing objects obtained via select in an IORMTable. Typically you only need this method after doing a select with one or more joins.

Important args:

  • $result - the database result
$result = $dbr->selectRow( /* stuff with one or more joins */ );

if ( $result !== false ) {
    $myCourse = $courseTable->newRowFromDBResult( $result );
}

ORMRow[edit]

The below list of methods is meant to give you an idea of how to use the class. For a complete overview with full documentation, see the source of the IORMRow interface.

Instantiation[edit]

New ORMRows are typically constructed via their IORMTable implementing class which acts as a factory, through the newRow method. You can call this yourself, although this is only needed when creating instances of objects you do not want to obtain from the database (or cannot in case of new objects). Direct construction of IORMRow implementing objects is discouraged.

Wrapper methods[edit]

getField[edit]

Returns the value of a set field.

Args:

  • $name - the name of the field to get. Field name without prefix.
  • $default - value to use when the field is not set. When null (=default) the function will complain if the field is not loaded.

Usage:

$course->getField( 'term' );

Returns: mixed, whatever the field type is. The class makes sure that fields are of the right type, so if an int field got set to user input '1', it'll be an int, not a string. Types include int, float, string, array, blob and boolean.

getFields[edit]

Returns the set fields.

Usage:

$course->getFields();

Returns

array(
    'foo' => $mixed0,
    'bar' => $mixed1,
    'baz' => $mixed2,
    // ...
)

setField[edit]

Sets the value of a field.

Args:

  • $name - the name of the field to set. Field name without prefix.
  • $value - value to set. Mixed. Function takes care of type conversions. ie an int field that gets passed '42' (string) will end up with 42 (int).

Usage:

$course->setField( 'term', 'q1 2012' );

setFields[edit]

Sets the value of a set of fields.

Args:

  • $fields - the values to set. Keys are field names, values are the values. Field names without prefix.
  • $override - Boolean, optional, true by default. If fields that are already set should be set to the newly provided values.

Usage:

$course->setFields( array( 'term' => 'q1 2012', 'awesome' => true, 'student_count' => 42 ) );

Query methods[edit]

save[edit]

Stores the object in the db. If it's a new object, it's inserted, if the object already has an associated ID, it's updated.

Usage:

$course->save();

remove[edit]

Removed the object from the database.

Usage:

$course->remove();

ORMResult[edit]

ORMResult is a class implementing ORMIterator, which is an interface extending Iterator, guaranteeing that the objects it contains implement IORMRow. It is returned by various select methods in IORMTable, most notably IORMTable::select.

If you're not familiar with iterators: you can loop over them as arrays as follows:

$courses = $courseTable->select( /* ... */ );

foreach ( $courses as $course ) {
    // $course implements IORMRow
    // And if you did everything right should be of type Course or similar.
}

If you want an actual array, you can feed the iterator to iterator_to_array.

Inheritance and composition[edit]

There are various places where you have some kind of functionality that has some differences in behavior which can be derived from the type of object it's handling, where one can write generic code that extends or composites a EPDBObject instead of duplicating common functionality over a file per object type. These are classes that already do this:

Example implementations[edit]

Extensions using this[edit]

Extensions using predecessor code

Notes[edit]

  1. Imagine a surveys table with fields survey_start_date and survey_end_date. "survey_" is the field prefix. In general it only is specified in its corresponding ORMTable class.
  2. Queries with joins cannot be nicely handled using the ORMTable abstraction so are best written as usual. They are the only place where the table name and field prefixes of a table should be specified outside of the table class.

See also[edit]