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