Key/value tables and how to use them in CakePHP 1.3

The key/value tables (EAV model) are a nice a approach to database modeling, when we need to store some arbitrary data about another model.

For example, let’s take a User model.
We could create a single users table to hold all of the potential data or even create an additional table such as user_details with all additional fields, which “might” be needed.

However, what happens when we don’t exactly know how much or what data will be eventually required by the application?

  • How many phones does the user have? Should we create a table with fields phone1, phone2, phone3, etc.?
  • What about emails? (Some users have 1 some have 5)
  • What happens if the business later on decides to ask for a fax number?.. and person’s height or favorite color?

If we attempt to predefine all the place-holders (columns or fields in a table) for the potential data, the table can grow horizontally into an unmanageable monster.

This is the perfect time to enter key/value tables.

Let’s consider our User model and, therefore the users table:
[sourcecode language=”sql”]
CREATE TABLE `users` (
`id` char(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT ”,
`username` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`password` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`created` datetime DEFAULT NULL,
`modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
[/cc]

The table couldn’t be simpler. All we’ll store here is the user’s login information. The rest will be handled by the user_details table…
It’s worth to mention that our models will have a pretty standard association:
User hasMany UserDetail and on the flip side UserDetail belongsTo User

Alright, next, we have the user_details table:
[sourcecode language=”sql”]
CREATE TABLE `user_details` (
`id` char(36) COLLATE utf8_unicode_ci NOT NULL DEFAULT ”,
`user_id` char(36) COLLATE utf8_unicode_ci DEFAULT NULL,
`field` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
`value` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
[/cc]

The columns field and value will actually store the user details. Because this table is going to grow vertically, potentially we can store an unlimited and completely arbitrary information about a user. As a matter of fact two different parts of your app can handle storing the user details completely independent of each other. For example, when a user plays the role of a “Guest” versus a user who represents a “Member”, or better yet… a user who is not event a human being, but rather a corporate entity, in which case typical fields like first_name and last_name, may become completely irrelevant.

Hopefully you can start seeing how the key/value approach can help our modeling be a lot more flexible.

… but let’s look closer at some details… how do we deal with this in cake-like-ways?

Actually, it is not that different from what you are already accustomed to.

Let’s see our UsersController:

class UsersController extends AppController {
 
  public function add () {
    if(!empty($this->data)) {
      $this->User->saveAll($this->data, array('validate' => 'first'));
    }
  }

}

Nothing unusual so far…

Next comes our add.ctp view:

echo $this->Form->create('User');
echo $this->Form->input('User.username');
echo $this->Form->input('UserDetail.0.dob');
echo $this->Form->input('UserDetail.1.gender', array(
                        'type' => 'radio',
                        'legend' => FALSE,
                        'options' => array(
                                  'M' => 'M',
                                  'F' => 'F'
                       )));
echo $this->Form->input('UserDetail.2.phone');
echo $this->Form->input('UserDetail.3.email');
echo $this->Form->end('Save');

Nothing unusual so far, yet again.
We are going to save a User with some username as well as some User details: date of birth, gender, phone and an email.
Thinking back, once the save happens, our user_details table will have four records for a single user ID.

Let’s see the models then:

User:

class User extends AppModel {
  public $hasMany = array('UserDetail');

  public $validate = array(
    'username' => array(
        'rule' => 'isUnique',
        'message' => 'Username already exists'
    )
  );
}

UserDetail:

class UserDetail extends AppModel {
  public $belongsTo = array('User');

  public $validate = array(
     'dob' => array(
        'rule' => 'notEmpty',
        'message' => 'Please select a date of birth'
     ),
     'gender' => array(
        'rule' => 'notEmpty',
        'message' => 'Please select a gender'
     ),
     'phone' => array(
        'rule' => 'phone',
        'message' => 'Please enter a phone'
     ),
     'email' => array(
        'rule' => 'email',
        'message' => 'Please enter a valid email'
     )
  );

  public function beforeSave() {
     foreach($this->data[$this->alias] as $field => $value) {
        if($field !== 'user_id') {
          $this->data[$this->alias]['field'] = $field;
          $this->data[$this->alias]['value'] = $value;
        }
     }
     return true;
  }
}

So the validation is handled “as always”.

The only little bit of trickery is in our beforeSave() method…
Here we take our existing fields from the data array (dob, gender, phone…), and convert them to the expected key/value pair (or field/value based on the column names). We are skipping the user_id which is automatically injected by saveAll() just as needed.

Well, this should be a good starting point…

The more extravagant example would be showing the ability to store validation rules and field types in the DB as well. This is helpful if you would like an admin back-end to manage the requirements for user details. As of this example, we’d have to manually adjust the views and validation rules to allow for any new fields. If we store the rules (as serialized array for example) as well as field type(s), the form building and validation would be done on the fly and would allow business users to manage the specifics of the required data.

… but that’s a topic for another day.

%d bloggers like this: