Difference between #13 and #19 of
UUID instead of an auto-increment integer for ID with Active Record

Changes

Title unchanged

UUID instead of an auto-increment integer for ID with Active Record

Category unchanged

How-tos

Yii version unchanged

2.0

Tags unchanged

mysql,active record,REST,UUID

Content changed

> I have a dream ... I am happy to join with you today in what will go down in history as the greatest demonstration of
 
 
bad design of Active Record.
 
 
I have an API. It's built with a RESTful extension over Active Record, and some endpoints provide PUT methods to upload files. By a REST design we create an entity with `POST /video` first, and then upload a video file with `PUT /video/{id}/data`. 
 
 
How do we get the `{id}`? The essential solutuion is UUID generated by a client. It allows API application to be stateless and scale it, use master-master replication for databases and feel yourself a modern guy.
 
If you have Postgres - lucky you, feel free to use the built-in UUID data type and close this article.
 
With MySQL the essential solution is [insert into users values(unhex(replace(uuid(),'-',''))...](https://mysqlserverteam.com/storing-uuid-values-in-mysql-tables/)
 
MySQL team recommends updating our INSERT queries. With Active Record it is not really possible.
 
For fetching UUIDs it recommends adding a virtual column - this can be used.
 
 
If you design the application from ground up, you can use defferent fields for a binary and text representation of UUID, and reference them in different parts of an application, but I am bound to the legacy code.
 
 
Adding `getId()`/`setId()` won't help - data comes from a client in JSON and fills the model object with a `setAttributes()` call avoiding generic magic methods.
 
 
Here's the hack:
 
 
Step 1. Add a `private $idText;` property
 
 
```php
 
use yii\db\ActiveRecord;
 
class Video extends ActiveRecord
 
{
 
    private $idText;
 
 
```
 
 
Step 2. Add two filters
 
 
```php
 
['id','match', 'pattern'=>'/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i'],
 
// convert UUID from text value to binary and store the text value in a private variable
 
// this is a workaround for lack of mapping in active record
 
['id','filter','skipOnError'=>true, 'filter'=>function($uuid){
 
         $this->idText = $uuid;
 
         return pack("H*", str_replace('-', '', $uuid));
 
     }],
 
```
 
These filters will validate input, prepare UUID to be written in a binary format and keep the text form for output.
 
 
Step 3. Add getters
 
 
```php
 
public function __get($name)
 
{
 
    return ($name === 'id') ? $this->getId() : parent::__get($name);
 
}
 
 
/**
 
 * Return UUID in a textual representation
 
 */
 
public function getId(): string
 
{
 
    if ($this->idText === NULL && $this->getIsNewRecord()){
 
        //the filter did not convert ID to binary yet, return the data from input
 
        return $this->getAttribute('id');
 
    }
 
    //ID is converted
 
    return $this->idText ?? $this->getAttribute('id_text');
 
}
 
```
 
 
Active Record does not call the getter method if attributes contain the property. It should not be this way, so I return the default component behavior and make ID returned the right way.
 
From the other hand, the first valiator calls `$model->id` triggering the getter before the UUID is saved to the private property so I need to serve the value from user input.
 
 
It is strange to mutate data in a validator, but I found this is the only way. I belive I shouldn't use `beforeSave()` callback to set the binary value for generating SQL, and return the text value back in `afterSave()` - supporting this code would be a classic hell like `#define true false;`.
 
 
Step 4. Define the mapping for output
 
 
```php
 
public function fields()
 
{
 
    $fields = parent::fields();
 
    $fields['id'] =function(){return $this->getId();};
 
    return $fields;
 
}
 
```
 
 
This method is used by RESTful serializers to format data when you access your API with `GET /video` requests.
 
 
So, now you can go the generic MySQL way
 
 
Step 5. add a virtual column
 
 
```sql
 
ALTER TABLE t1 ADD id_text varchar(36) generated always as
 
 (insert(
 
    insert(
 
      insert(
 
        insert(hex(id_bin),9,0,'-'),
 
        14,0,'-'),
 
      19,0,'-'),
 
    24,0,'-')
 
 ) virtual;
 
```
 
 
 
Step 5. Use Object Relation Mapping in Yii 3 when it's available and write siple mapping instead.
> I am happy to join with you today in what will go down in history as the greatest demonstration of bad design of Active Record.
 
 
I have an API. It's built with a RESTful extension over Active Record, and some endpoints provide PUT methods to upload files. By a REST design we create an entity with `POST /video` first, and then upload a video file with `PUT /video/{id}/data`. 
 
 
How do we get the `{id}`? The essential solutuion is UUID generated by a client. It allows API application to be stateless and scale it, use master-master replication for databases and feel yourself a modern guy.
 
If you have Postgres — lucky you, feel free to use the built-in UUID data type and close this article.
 
With MySQL the essential solution is [insert into users values(unhex(replace(uuid(),'-',''))...](https://mysqlserverteam.com/storing-uuid-values-in-mysql-tables/)
 
MySQL team recommends updating our INSERT queries. With Active Record it is not really possible.
 
For fetching UUIDs it recommends adding a virtual column — this can be used.
 
 
If you design the application from ground up, you can use defferent fields for a binary and text representation of UUID, and reference them in different parts of an application, but I am bound to the legacy code.
 
 
Adding `getId()`/`setId()` won't help - data comes from a client in JSON and fills the model object with a `setAttributes()` call avoiding generic magic methods.
 
 
Here's the hack:
 
 
Step 1. Add a private `$idText` property
 
 
```php
 
use yii\db\ActiveRecord;
 
class Video extends ActiveRecord
 
{
 
    private $idText;
 
 
```
 
 
Step 2. Add two validators and a filter
 
 
```php
 
//check if value is a valid UUID
 
['id','match', 'pattern'=>'/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i'],
 
// convert UUID from text value to binary and store the text value in a private variable,
 
// this is a workaround for lack of mapping in active record
 
['id','filter','skipOnError' => true, 'filter' => function($uuid) {
 
    $this->idText = $uuid;
 
    return pack("H*", str_replace('-', '', $uuid));
 
}],
 
//now let's check if ID is taken
 
['id','unique','filter' => function(\yii\db\Query $q) {
 
    $q->where(['id' => $this->getAttribute('id')]);
 
}],
 
```
 
 
First rule is a validator for an input. Second rule is a filter preparing UUID to be written in a binary format and keeping the text form for output. Third one is a validator running a query over the binary value generated by a filter.
 
 
> Note: I wrote `$this->getAttribute('id')`, `$this->id` returns a text form.
 
 
We can write a query to validate data, not to save it.
 
 
Step 3. Add getters
 
 
```php
 
public function __get($name)
 
{
 
    return ($name === 'id') ? $this->getId() : parent::__get($name);
 
}
 
 
/**
 
 * Return UUID in a textual representation
 
 */
 
public function getId(): string
 
{
 
    if ($this->idText === NULL && $this->getIsNewRecord()){
 
        //the filter did not convert ID to binary yet, return the data from input
 
        return strtoupper($this->getAttribute('id'));
 
    }
 
    //ID is converted
 
    return strtoupper($this->idText ?? $this->getAttribute('id_text'));
 
}
 
```
 
 
When we call the `$model->id` property we need the `getId()` executed. But Active Record base class overrides Yii compoent default behavior and does not call a getter method of an object if a property is a field in a table. So I override the magic getter.
 
From the other hand, a regexp valiator I wrote calls `$model->id`, triggering the getter before the UUID is saved to the private property. I check if the object is newly created to serve the text value for validator.
 
 
Note the `strtoupper()` call: client may send UUID in both upper and low cases, but after unpacking from binary we will have a value in upper case. I received different string values before storing data to DB and after fetching it. Convert the textual UUID value to an upper or lower case everywhere to avoid problems.
 
 
It looks weird to mutate data in a validator, but I found this is the best way. I belive I shouldn't use `beforeSave()` callback to set the binary value for generating SQL, and return the text value back in `afterSave()` - supporting this code would be a classic hell like `#define true false;`.
 
 
Step 4. Define the mapping for output
 
 
```php
 
public function fields()
 
{
 
    $fields = parent::fields();
 
    $fields['id'] =function(){return $this->getId();};
 
    return $fields;
 
}
 
```
 
 
This method is used by RESTful serializers to format data when you access your API with `GET /video` requests.
 
 
So, now you can go the generic MySQL way
 
 
Step 5. add a virtual column
 
 
```sql
 
ALTER TABLE t1 ADD id_text varchar(36) generated always as
 
 (insert(
 
    insert(
 
      insert(
 
        insert(hex(id_bin),9,0,'-'),
 
        14,0,'-'),
 
      19,0,'-'),
 
    24,0,'-')
 
 ) virtual;
 
```
 
 
 
Step 5. Use Object Relation Mapping in Yii 3 when it's available and write mapping instead of these hacks.
 
 
 
P.S. A couple of helper functions.
 
 
```php
 
declare(strict_types=1);
 
 
namespace common\helpers;
 
 
 
class UUIDHelper
 
{
 
    const UUID_REGEXP = '/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i';
 
 
    public static function string2bin(string $uuid): string
 
    {
 
        return pack("H*", str_replace('-', '', $uuid));
 
    }
 
 
    public static function bin2string(string $binary): string
 
    {
 
        return strtolower(join("-", unpack("H8time_low/H4time_mid/H4time_hi/H4clock_seq_hi/H12clock_seq_low", $binary)));
 
    }
 
 
    public static function isUUID(string $uuid): bool
 
    {
 
        return (bool)preg_match(self::UUID_REGEXP,$uuid);
 
    }
 
}
 
```
 
4 0
3 followers
Viewed: 22 492 times
Version: 2.0
Category: How-tos
Written by: grigori
Last updated by: samdark
Created on: Nov 25, 2019
Last updated: 7 months ago
Update Article

Revisions

View all history