Single Table Inheritance

Ryan Durham
Stage Right Labs

ryan at stagerightlabs dot com

stagerightlabs

What is Single Table Inheritance?

Single Table Inheritance

"Represents an inheritance hierarchy of classes as a
single table that has columns for all the fields
of the various classes."

- Martin Fowler, Patterns of
Enterprise Application Architecture

In Practice:

  • Using one database table to store multiple object types.
  • With Data Mapper: Not much of a mental leap.
  • With Active Record: A very radical and unusual idea.

What benefits can Single Table Inheritance provide?

Let's look at an example.

Example Scenario

Imagine we are tracking medical records.

  • Customers create orders
  • Record retrieval request is sent
  • Nurses review records and create summaries
  • Reports are released to the client
  • Client credit card is billed


The status of the record request dictates the type of actions that can be taken against it.


<?php foreach($records as $record) ?>
<tr>
<td><?php echo $record->referenceNum; ?></td>
<td>
<?php if ($record->status == 'new') ?>
  <a href="...">Cancel</a>
  <a href="...">Assign to Nurse</a>
<?php endif; ?>
<?php if ($record->status == 'acquired') ?>
  <a href="...">Prepare Summary</a>
  <a href="...">View PDFs</a>
<?php endif; ?>
<?php if ($record->status == 'summarized') ?>
  <a href="...">Review Summary</a>
  <a href="...">Approve</a>
  <a href="...">Reject</a>
<?php endif; ?>
<?php if ($record->status == 'approved') ?>
  <a href="...">View</a>
  <a href="...">Process Payment</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
  

This works, but the view code is very cluttered, and we are not even accounting for user access control.

Is there a better way?

Single Table Inheritance is an excellent alternative.

By implementing STI we can ask our entity objects to define their own allowable actions.

For instance:


public function getActions($userLevel)
{
return $this->actions[$userLevel];
}

protected $actions = [
'client' => [
[
  'name'    => 'edit',
  'action'  => 'ClientReportController@edit',
  'class'   => 'default'
],
[
  'name'    => 'delete',
  'action'  => 'ClientReportController@delete',
  'class'   => 'danger'
],
]
// Additional User level entries ...
];
  

Updated Example


<?php foreach($records as $record) ?>
<tr>
<td>
<?php echo $record->referenceNum; ?>
</td>
<td>
<?php echo $record->getActions('client'); ?>
</td>
</tr>
<?php endforeach; ?>
  

Definitions:

Discriminator Column: The table column used to determine the entity type.

Inheritance Map: A way to associate discrimination values with php classes.

Using STI with Eloquent

Eloquent Model with STI


// app/Epiphyte/Record.php
class Record extends Illuminate\Database\Eloquent\Model {

// Eloquent Configuration
protected $guarded = ['id'];
protected $fillable = ['name', 'description', 'status'];

// Single Table Inheritance Configuration
use SingleTableInheritanceTrait;
protected $table = 'records';
protected $morphClass = 'Epiphyte\Record';
protected $discriminatorColumn = 'status';
protected $inheritanceMap = [
  'new' => 'Epiphyte\Entities\Records\RecordNew',
  'requested' => 'Epiphyte\Entities\Records\RecordRequested',
  'retrieved' => 'Epiphyte\Entities\Records\RecordRetrieved',
  'complete' => 'Epiphyte\Entities\Records\RecordComplete'
];

// ...
}
  

Child Entity Class


// app/Epiphyte/Entities/Records/RecordNew.php
class RecordNew extends Epiphyte\Record {

/**
* Limit the query scope if we define a query against
* the base table using this class.
*/
public function newQuery($excludeDeleted = true)
{
  return parent::newQuery($excludeDeleted)
  ->where('status', '=', 'new');
}

// Remaining child entity methods go here...

}
    

STI in Eloquent - Data Mapping


// Illuminate\Database\Eloquent\Model
public function mapData(array $attributes)
{
// Determine the entity type
$entityType = isset($attributes[$this->discriminatorColumn]) ?
$attributes[$this->discriminatorColumn] : null;

// Throw an exception if this entity type
// is not in the inheritance map
if (!array_key_exists($entityType, $this->inheritanceMap)) {
  throw new ModelNotFoundException();
}

// Get the appropriate class name from
// the inheritance map
$class = $this->inheritanceMap[$entityType];

// Return a new instance of the specified class
return new $class;
}
  

STI in Eloquent - Query Builder


// Illuminate\Database\Eloquent\Model
public function newFromBuilder($attributes = array())
{
// Create a new instance of the Entity Type Class
$m = $this->mapData((array)$attributes)
              ->newInstance(array(), true);

// Hydrate the new instance with the table data
$m->setRawAttributes((array)$attributes, true);

// Return the assembled object
return $m;
}
    

What have we gained?

  • Eloquent will now automatically resolve entity types based on the discrimination value.
  • Collections will now contain a mix of entity types, dictated by the discrimination value.
  • Facade convenience, if you are in to that sort of thing: RecordNew::all();

Using STI with Doctrine

The Data Mapper pattern lends itself more easily to Single Table Inheritance

Doctrine Example


/**
* @Entity
* @InheritanceType("SINGLE_TABLE")
* @DiscriminatorColumn(name="discr", type="string")
* @DiscriminatorMap({"person" = "Person", "employee" = "Employee"})
*/
class Person
{
// ...
}

/**
* @Entity
*/
class Employee extends Person
{
// ...
}
  

Some important considerations

STI is ideal for situations where the child entities all make use of the same table column structure.

Good:

Inheriting Ford, Toyota and Mercedes types from an automobiles table.

Bad:

Inheriting Car, Motorcyle and Airplane types from a vehicles table.

Adding table columns that will only be used by specific child entities is a warning sign.

STI may not be a good choice in that case.

Questions?

Additional Resources

Thank you!

ryan at stagerightlabs dot com

stagerightlabs

Stage Right Labs Logo