Diviner

Class LiskDAO

Definedsrc/infrastructure/storage/lisk/LiskDAO.php:167
GroupStorage

Simple object-authoritative data access object that makes it easy to build stuff that you need to save to a database. Basically, it means that the amount of boilerplate code (and, particularly, boilerplate SQL) you need to write is greatly reduced.

Lisk makes it fairly easy to build something quickly and end up with reasonably high-quality code when you're done (e.g., getters and setters, objects, transactions, reasonably structured OO code). It's also very thin: you can break past it and use MySQL and other lower-level tools when you need to in those couple of cases where it doesn't handle your workflow gracefully.

However, Lisk won't scale past one database and lacks many of the features of modern DAOs like Hibernate: for instance, it does not support joins or polymorphic storage.

This means that Lisk is well-suited for tools like Differential, but often a poor choice elsewhere. And it is strictly unsuitable for many projects.

Lisk's model is object-authoritative: the PHP class definition is the master authority for what the object looks like.

Building New Objects

To create new Lisk objects, extend LiskDAO and implement establishLiveConnection(). It should return an AphrontDatabaseConnection; this will tell Lisk where to save your objects.

class Dog extends LiskDAO {

  protected $name;
  protected $breed;

  public function establishLiveConnection() {
    return $some_connection_object;
  }
}

Now, you should create your table:

CREATE TABLE dog (
  id int unsigned not null auto_increment primary key,
  name varchar(32) not null,
  breed varchar(32) not null,
  dateCreated int unsigned not null,
  dateModified int unsigned not null
);

For each property in your class, add a column with the same name to the table (see getConfiguration() for information about changing this mapping). Additionally, you should create the three columns id, dateCreated and dateModified. Lisk will automatically manage these, using them to implement autoincrement IDs and timestamps. If you do not want to use these features, see getConfiguration() for information on disabling them. At a bare minimum, you must normally have an id column which is a primary or unique key with a numeric type, although you can change its name by overriding getIDKey() or disable it entirely by overriding getIDKey() to return null. Note that many methods rely on a single-part primary key and will no longer work (they will throw) if you disable it.

As you add more properties to your class in the future, remember to add them to the database table as well.

Lisk will now automatically handle these operations: getting and setting properties, saving objects, loading individual objects, loading groups of objects, updating objects, managing IDs, updating timestamps whenever an object is created or modified, and some additional specialized operations.

Creating, Retrieving, Updating, and Deleting

To create and persist a Lisk object, use save():

$dog = id(new Dog())
  ->setName('Sawyer')
  ->setBreed('Pug')
  ->save();

Note that Lisk automatically builds getters and setters for all of your object's protected properties via __call(). If you want to add custom behavior to your getters or setters, you can do so by overriding the readField() and writeField() methods.

Calling save() will persist the object to the database. After calling save(), you can call getID() to retrieve the object's ID.

To load objects by ID, use the load() method:

$dog = id(new Dog())->load($id);

This will load the Dog record with ID $id into $dog, or null if no such record exists (load() is an instance method rather than a static method because PHP does not support late static binding, at least until PHP 5.3).

To update an object, change its properties and save it:

$dog->setBreed('Lab')->save();

To delete an object, call delete():

$dog->delete();

That's Lisk CRUD in a nutshell.

Queries

Often, you want to load a bunch of objects, or execute a more specialized query. Use loadAllWhere() or loadOneWhere() to do this:

$pugs = $dog->loadAllWhere('breed = %s', 'Pug');
$sawyer = $dog->loadOneWhere('name = %s', 'Sawyer');

These methods work like queryfx(), but only take half of a query (the part after the WHERE keyword). Lisk will handle the connection, columns, and object construction; you are responsible for the rest of it. loadAllWhere() returns a list of objects, while loadOneWhere() returns a single object (or null).

There's also a loadRelatives() method which helps to prevent the 1+N queries problem.

Managing Transactions

Lisk uses a transaction stack, so code does not generally need to be aware of the transactional state of objects to implement correct transaction semantics:

$obj->openTransaction();
  $obj->save();
  $other->save();
  // ...
  $other->openTransaction();
    $other->save();
    $another->save();
  if ($some_condition) {
    $other->saveTransaction();
  } else {
    $other->killTransaction();
  }
  // ...
$obj->saveTransaction();

Assuming $obj, $other and $another live on the same database, this code will work correctly by establishing savepoints.

Selects whose data are used later in the transaction should be included in beginReadLocking() or beginWriteLocking() block.

Tasks

Managing Connections

Configuring Lisk

Loading Objects

Examining Objects

Writing Objects

Hooks and Callbacks

Utilities

Managing Transactions

Isolation for Unit Testing

Unspecified

Methods

public mixed __call($method, $args)

parametersstring$methodMethod name.
list$argsArgument vector.
returnmixedget*() methods return the property value. set*() methods return $this.

Black magic. Builds implied get*() and set*() for all properties.

public this __construct()

returnthis

Build an empty object.

@return obj Empty object.

public __set($name, $value)

parameterswild$name
wild$value
returnwild

Warns against writing to undeclared property.

protected applyLiskDataSerialization(array &$data, $deserialize)

parametersarray&$data
wild$deserialize
returnwild

Applies configured serialization to a dictionary of values.

public static beginIsolateAllLiskEffectsToCurrentProcess()

returnwild
This method is not documented.

public static beginIsolateAllLiskEffectsToTransactions()

returnwild
This method is not documented.

public this beginReadLocking()

returnthis

Begins read-locking selected rows with SELECT ... FOR UPDATE, so that other connections can not read them (this is an enormous oversimplification of FOR UPDATE semantics; consult the MySQL documentation for details). To end read locking, call endReadLocking(). For example:

$beach->openTransaction();
  $beach->beginReadLocking();

    $beach->reload();
    $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1);
    $beach->save();

  $beach->endReadLocking();
$beach->saveTransaction();

public this beginWriteLocking()

returnthis

Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so that other connections can not update or delete them (this is an oversimplification of LOCK IN SHARE MODE semantics; consult the MySQL documentation for details). To end write locking, call endWriteLocking().

final protected call($method, $args)

parameterswild$method
wild$args
returnwild
This method is not documented.

protected string|null checkProperty($property)

parameterswild$property
returnstring|nullCanonical property name, or null if the property does not exist.

Check if a property exists on this object.

public static closeAllConnections()

returnwild
This method is not documented.

public this delete()

returnthis

Delete this object, permanently.

protected didDelete()

returnwild

Hook to perform an action after the deletion of an object.

protected didReadData()

returnwild

Hook to perform an action on data after it is read from the database.

protected didWriteData()

returnwild

Hook to perform actions after data has been written to the database.

public static endIsolateAllLiskEffectsToCurrentProcess()

returnwild
This method is not documented.

public static endIsolateAllLiskEffectsToTransactions()

returnwild
This method is not documented.

public this endReadLocking()

returnthis

Ends read-locking that began at an earlier beginReadLocking() call.

public this endWriteLocking()

returnthis

Ends write-locking that began at an earlier beginWriteLocking() call.

public LiskDatabaseConnection establishConnection($mode, $force_new = false)

parametersstring$mode'r' for read, 'w' for read/write.
bool$force_newTrue to force a new connection. The connection will not be retrieved from or saved into the connection cache.
returnLiskDatabaseConnectionLisk connection object.

Get or build the database connection for this object.

private establishIsolatedConnection($mode)

parameterswild$mode
returnwild
This method is not documented.

protected abstract AphrontDatabaseConnection establishLiveConnection($mode)

parametersstring$modeMode, either 'r' (reading) or 'w' (reading and writing).
returnAphrontDatabaseConnectionNew database connection.

Establish a live connection to a database service. This method should return a new connection. Lisk handles connection caching and management; do not perform caching deeper in the stack.

protected phid generatePHID()

returnphidUnique, newly allocated PHID.

Generate a new PHID, used by CONFIG_AUX_PHID.

public getConfigOption($option_name)

parameterswild$option_name
returnwild

Determine the setting of a configuration option for this class of objects.

@param const Option name, one of the CONFIG_* constants. @return mixed Option value, if configured (null if unavailable).

@task config

protected dictionary getConfiguration()

returndictionaryMap of configuration options to values.

Change Lisk behaviors, like ID configuration and timestamps. If you want to change these behaviors, you should override this method in your child class and change the options you're interested in. For example:

public function getConfiguration() {
  return array(
    Lisk_DataAccessObject::CONFIG_EXAMPLE => true,
  ) + parent::getConfiguration();
}

The available options are:

CONFIG_IDS Lisk objects need to have a unique identifying ID. The three mechanisms available for generating this ID are IDS_AUTOINCREMENT (default, assumes the ID column is an autoincrement primary key), IDS_MANUAL (you are taking full responsibility for ID management), or IDS_COUNTER (see below).

InnoDB does not persist the value of auto_increment across restarts, and instead initializes it to MAX(id) + 1 during startup. This means it may reissue the same autoincrement ID more than once, if the row is deleted and then the database is restarted. To avoid this, you can set an object to use a counter table with IDS_COUNTER. This will generally behave like IDS_AUTOINCREMENT, except that the counter value will persist across restarts and inserts will be slightly slower. If a database stores any DAOs which use this mechanism, you must create a table there with this schema:

CREATE TABLE lisk_counter (
  counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY,
  counterValue BIGINT UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CONFIG_TIMESTAMPS Lisk can automatically handle keeping track of a `dateCreated' and `dateModified' column, which it will update when it creates or modifies an object. If you don't want to do this, you may disable this option. By default, this option is ON.

CONFIG_AUX_PHID This option can be enabled by being set to some truthy value. The meaning of this value is defined by your PHID generation mechanism. If this option is enabled, a `phid' property will be populated with a unique PHID when an object is created (or if it is saved and does not currently have one). You need to override generatePHID() and hook it into your PHID generation mechanism for this to work. By default, this option is OFF.

CONFIG_SERIALIZATION You can optionally provide a column serialization map that will be applied to values when they are written to the database. For example:

self::CONFIG_SERIALIZATION => array(
  'complex' => self::SERIALIZATION_JSON,
)

This will cause Lisk to JSON-serialize the 'complex' field before it is written, and unserialize it when it is read.

CONFIG_PARTIAL_OBJECTS Sometimes, it is useful to load only some fields of an object (such as when you are loading all objects of a class, but only care about a few fields). Turning on this option (by setting it to a truthy value) allows users of the class to create/use partial objects, but it comes with some side effects: your class cannot override the setters and getters provided by Lisk (use readField and writeField instead), and you should not directly access or assign protected members of your class (use the getters and setters).

protected abstract string getConnectionNamespace()

returnstringConnection namespace for cache

Return a namespace for this object's connections in the connection cache. Generally, the database name is appropriate. Two connections are considered equivalent if they have the same connection namespace and mode.

protected AprontDatabaseConnection|null getEstablishedConnection($mode)

parametersmode$modeConnection mode.
returnAprontDatabaseConnection|nullConnection, if it exists in cache.

Get an existing, cached connection for this object.

public mixed getID()

returnmixedUnique ID.

Retrieve the unique ID identifying this object. This value will be null if the object hasn't been persisted and you didn't set it manually.

public string getIDKey()

returnstringName of the ID column.

Retrieve the primary key column, "id" by default. If you can not reasonably name your ID column "id", override this method.

protected getIDKeyForUse()

returnwild
This method is not documented.

final protected getInSet()

returnwild
This method is not documented.

protected dict getProperties()

returndictDictionary of normalized (lowercase) to canonical (original case) property names.

Retrieve a list of all object properties. This list only includes properties that are declared as protected, and it is expected that all properties returned by this function should be persisted to the database. Properties that should not be persisted must be declared as private.

protected dict getPropertyValues()

returndictDictionary of object properties.

Convert this object into a property dictionary. This dictionary can be restored into an object by using loadFromArray() (unless you're using legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you should just go ahead and die in a fire).

public string getTableName()

returnstringTable name for object storage.

Retrieve the database table name. By default, this is the class name.

public bool hasProperty($property)

parametersstring$propertyProperty name.
returnboolTrue if the property exists.

Test if a property exists.

public insert()

returnwild

Save this object, forcing the query to use INSERT regardless of object state.

@return this

@task save

protected insertRecordIntoDatabase($mode)

parametersconst$modeEither "INSERT" or "REPLACE", to force the desired mode.
returnwild

Internal implementation of INSERT and REPLACE.

private isEphemeralCheck()

returnwild
This method is not documented.

public this killTransaction()

returnthis

Decrease transaction stack depth, discarding work.

public obj|null load($id)

parametersint$idNumeric ID identifying the object to load.
returnobj|nullIdentified object, or null if it does not exist.

Load an object by ID. You need to invoke this as an instance method, not a class method, because PHP doesn't have late static binding (until PHP 5.3.0). For example:

$dog = id(new Dog())->load($dog_id);

public dict loadAll()

returndictDictionary of all persisted objects of this type, keyed on object ID.

Loads all of the objects, unconditionally.

public dict loadAllFromArray(array $rows)

parameterslist$rowsList of property dictionaries.
returndictList of constructed objects, keyed on ID.

Initialize a list of objects from a list of dictionaries. Usually you load lists of objects with loadAllWhere(), but sometimes that isn't flexible enough. One case is if you need to do joins to select the right objects:

function loadAllWithOwner($owner) {
  $data = $this->queryData(
    'SELECT d.*
      FROM owner o
        JOIN owner_has_dog od ON o.id = od.ownerID
        JOIN dog d ON od.dogID = d.id
      WHERE o.id = %d',
    $owner);
  return $this->loadAllFromArray($data);
}

This is a lot messier than loadAllWhere(), but more flexible.

public dict loadAllWhere($pattern, )

parametersstring$patternqueryfx()-style SQL WHERE clause.
...Zero or more conversions.
returndictDictionary of matching objects, keyed on ID.

Load all objects which match a WHERE clause. You provide everything after the 'WHERE'; Lisk handles everything up to it. For example:

$old_dogs = id(new Dog())->loadAllWhere('age > %d', 7);

The pattern and arguments are as per queryfx().

public dict loadColumns(array $columns)

parametersarray$columnsArray of canonical column names as strings
returndictDictionary of all objects, keyed by ID.

Loads all objects, but only fetches the specified columns.

public dict loadColumnsWhere(array $columns, $pattern, )

parametersarray$columnsList of column names.
string$patternqueryfx()-style SQL WHERE clause.
...Zero or more conversions.
returndictDictionary of matching objecks, keyed by ID.

Loads selected columns from objects that match a WHERE clause. You must provide everything after the WHERE. See loadAllWhere().

public this loadFromArray(array $row)

parametersdict$rowDictionary of properties, which should be equivalent to selecting a row from the table or calling getProperties().
returnthis

Initialize this object's properties from a dictionary. Generally, you load single objects with loadOneWhere(), but sometimes it may be more convenient to pull data from elsewhere directly (e.g., a complicated join via queryData()) and then load from an array representation.

public static int loadNextCounterID(AphrontDatabaseConnection $conn_w, $counter_name)

parametersAphrontDatabaseConnection$conn_wDatabase where the counter resides.
string$counter_nameCounter name to create or increment.
returnintNext counter value.

Increments a named counter and returns the next value.

final public LiskDAO loadOneRelative(LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '')

parametersLiskDAO$objectType of objects to load.
string$foreign_columnName of the column in target table.
string$key_methodMethod name in this table.
string$whereAdditional constraints on returned rows. It supports no placeholders and requires putting the WHERE part into parentheses. It's not possible to use LIMIT.
returnLiskDAOObject of type $object or null if there's no such object.

Load referenced row. See loadRelatives() for details.

public obj|null loadOneWhere($pattern, )

parametersstring$patternqueryfx()-style SQL WHERE clause.
...Zero or more conversions.
returnobj|nullMatching object, or null if no object matches.

Load a single object identified by a 'WHERE' clause. You provide everything after the 'WHERE', and Lisk builds the first half of the query. See loadAllWhere(). This method is similar, but returns a single result instead of a list.

protected loadRawDataWhere($columns, $pattern)

parameterswild$columns
wild$pattern
returnwild
This method is not documented.

public list loadRelatives(LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '')

parametersLiskDAO$objectType of objects to load.
string$foreign_columnName of the column in target table.
string$key_methodMethod name in this table.
string$whereAdditional constraints on returned rows. It supports no placeholders and requires putting the WHERE part into parentheses. It's not possible to use LIMIT.
returnlistObjects of type $object.

This method helps to prevent the 1+N queries problem. It happens when you execute a query for each row in a result set. Like in this code:

Easy to write but expensive to execute
$diffs = id(new DifferentialDiff())->loadAllWhere(
  'revisionID = %d',
  $revision->getID());
foreach ($diffs as $diff) {
  $changesets = id(new DifferentialChangeset())->loadAllWhere(
    'diffID = %d',
    $diff->getID());
  // Do something with $changesets.
}

One can solve this problem by reading all the dependent objects at once and assigning them later:

Cheaper to execute but harder to write and maintain
$diffs = id(new DifferentialDiff())->loadAllWhere(
  'revisionID = %d',
  $revision->getID());
$all_changesets = id(new DifferentialChangeset())->loadAllWhere(
  'diffID IN (%Ld)',
  mpull($diffs, 'getID'));
$all_changesets = mgroup($all_changesets, 'getDiffID');
foreach ($diffs as $diff) {
  $changesets = idx($all_changesets, $diff->getID(), array());
  // Do something with $changesets.
}

The method loadRelatives() abstracts this approach which allows writing a code which is simple and efficient at the same time:

Easy to write and cheap to execute
$diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
foreach ($diffs as $diff) {
  $changesets = $diff->loadRelatives(
    new DifferentialChangeset(),
    'diffID');
  // Do something with $changesets.
}

This will load dependent objects for all diffs in the first call of loadRelatives() and use this result for all following calls.

The method supports working with set of sets, like in this code:

$diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
foreach ($diffs as $diff) {
  $changesets = $diff->loadRelatives(
    new DifferentialChangeset(),
    'diffID');
  foreach ($changesets as $changeset) {
    $hunks = $changeset->loadRelatives(
      new DifferentialHunk(),
      'changesetID');
    // Do something with hunks.
  }
}

This code will execute just three queries - one to load all diffs, one to load all their related changesets and one to load all their related hunks. You can try to write an equivalent code without using this method as a homework.

The method also supports retrieving referenced objects, for example authors of all diffs (using shortcut loadOneRelative()):

foreach ($diffs as $diff) {
  $author = $diff->loadOneRelative(
    new PhabricatorUser(),
    'phid',
    'getAuthorPHID');
  // Do something with author.
}

It is also possible to specify additional conditions for the WHERE clause. Similarly to loadAllWhere(), you can specify everything after WHERE (except LIMIT). Contrary to loadAllWhere(), it is allowed to pass only a constant string (% doesn't have a special meaning). This is intentional to avoid mistakes with using data from one row in retrieving other rows. Example of a correct usage:

$status = $author->loadOneRelative(
  new PhabricatorUserStatus(),
  'userPHID',
  'getPHID',
  '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)');

public makeEphemeral()

returnwild

Make an object read-only.

Making an object ephemeral indicates that you will be changing state in such a way that you would never ever want it to be written back to the storage.

public this openTransaction()

returnthis

Increase transaction stack depth.

final public putInSet(LiskDAOSet $set)

parametersLiskDAOSet$set
returnwild
This method is not documented.

protected mixed readField($field)

parametersstring$fieldCanonical field name
returnmixedValue of the field

Reads the value from a field. Override this method for custom behavior of getField() instead of overriding getField directly.

public this reload()

returnthis

Reload an object from the database, discarding any changes to persistent properties. This is primarily useful after entering a transaction but before applying changes to an object.

public this replace()

returnthis

Save this object, forcing the query to use REPLACE regardless of object state.

private resetDirtyFields()

returnwild

Resets the dirty fields (fields which need to be written on next save/ update/insert/replace). If this DAO has timestamps, the modified time is always a dirty field.

public this save()

returnthis

Persist this object to the database. In most cases, this is the only method you need to call to do writes. If the object has not yet been inserted this will do an insert; if it has, it will do an update.

public this saveTransaction()

returnthis

Decrease transaction stack depth, saving work.

protected this setEstablishedConnection($mode, AphrontDatabaseConnection $connection, $force_unique = false)

parametersmode$modeConnection mode.
AphrontDatabaseConnection$connectionConnection to cache.
wild$force_unique
returnthis

Store a connection in the connection cache.

public this setID($id)

parametersmixed$idUnique ID.
returnthis

Set unique ID identifying this object. You normally don't need to call this method unless with IDS_MANUAL.

protected bool shouldInsertWhenSaved()

returnbooltrue if the record should be inserted

Method used to determine whether to insert or update when saving.

public static shouldIsolateAllLiskEffectsToCurrentProcess()

returnwild
This method is not documented.

public static shouldIsolateAllLiskEffectsToTransactions()

returnwild
This method is not documented.

public update()

returnwild

Save this object, forcing the query to use UPDATE regardless of object state.

@return this

@task save

protected willDelete()

returnwild

Hook to perform an action before the deletion of an object.

protected willReadData(array &$data)

parametersarray&$data
returnwild

Hook to apply serialization or validation to data as it is read from the database. See also willWriteData().

protected willSaveObject()

returnwild

Hook to make internal object state changes prior to INSERT, REPLACE or UPDATE.

protected willWriteData(array &$data)

parametersarray&$data
returnwild

Hook to apply serialization or validation to data before it is written to the database. See also willReadData().

protected writeField($field, $value)

parametersstring$fieldCanonical field name
mixed$valueValue to write
returnwild

Writes a value to a field. Override this method for custom behavior of setField($value) instead of overriding setField directly.