Skip to content

Persistence

Romans Malinovskis edited this page Jul 1, 2016 · 2 revisions

In application design, specifically for a large enterprise apps, the ability to separate business logic from persistence logic is essential.

Some of the approaches solve the prolbem by creating in-memory model and then defining custom queries for each entity to save, load, update or delete from the database. In practice this approach is tedious, prone to errors and very inefficient.

Agile Data offers the more universal approach.

Persistence Drivers

A class 'Persistence' defines a common logic and interface that is then extended thtrough the number of other classes:

  • Persistence
    • Persistence_SQL
    • Persistence_Array
    • Persistence_MongoDB

The persistence drivers are designed to handle interpretation of your business logic (fields, joins, expressions, etc) and converting those into queries for a specific database vendor.

The capabilities of a driver include ability to store, update or fetch data on recod-by-record level as well as handling exceptions.

Adding New Persistence

Typically you only need one persistence which you can share globally throughout your project. Here is example how you can create Array-based persistence:

$ar = [];
$db_array = new atk4\data\Persistence_Arary($ar);

The actual array will be passed as a reference and updated as you operate. Let's create a model and do some work with it:

$m = new Model($db_array, 'user');
$m->addFields(['name','email']);

$m['name'] = 'John';
$m['email'] = 'john@example.com';
$m->save();

var_Dump($ar);  
// will give you ['user'=>[ 1=>['name'=>'John','email'=>'john@example.com'] ]]

The way how I have defined Model above is called "inline definition" - it wasn't done through a class and I simply constructed a quick model on the fly. A better way is if you define your business logic first then use the model:

$m = new Model_User($db_array);

$m->set(['name'=>'Joe', 'surname'=>'Blogs']);
$m->save();

Connecting to SQL

Next continue your code to create a different persistance, this time using SQL. I'll be using in-memory SQLite, but you can also use MySQL.

$db_sqlite = new atk4\data\Persistence_SQL('sqlite::memory:');

// using connect()
$db_mysql = atk4\data\Persistence::connect(   // see PDO for DSN format
    'mysql:host=localhost;dbname=test',
    'root', 'root'
);

As you have probably guessed, you can now create new instance of the model and save it into SQL:

$m = new Model_User($db_sqlite);

$m->set(['name'=>'Joe', 'surname'=>'Blogs']);
$m->save();

Agile Data is designed from the ground-up to work with multiple persistances. You get to decide where you persist your model data and Agile Data will optimize queries for you.

Moving model betwen persistences

TODO: (this is not implemented yet)

$m = new Model_User($db_sqlite);
$m->load(1);
$m_array = $m->saveInto($db_array);

THE REST OF THIS DOC IS OBSOLETE

We offer you ability to tweak how your Business Models are stored.

So far you know that model definition contains $table property and that we use the $db object's "add" method to create instance of an object. The reason why we do that is to "link" this Model with that specific database connection:

class Model_Admin extends Model_User
{
    function init() {
        parent::init();

        $this->addCondition('type', 'admin');

        $this->addField('can_edit_users')->type('boolean');
        $this->addField('can_edit_orders')->type('boolean');
    }
}
$users = $mysql_db->add('Model_Admin');
$users -> tryLoadAny();

The goal of DB->PM mapping is to make sure that you don't have to change your code if your database model has been changed.

Let's think what we could do if we refactor our "user" table by splitting "can_edit_users" and "can_edit_orders" into a separate table called "admin_permission". Now in order to retrieve data from this table we have to perform a join. More importantly, when new Admin record is created, both tables have to be populated.

Thankfully after your database changes you will only have to change your code a little:

class Model_Admin extends Model_User
{
    function init() {
        parent::init();

        $this->addCondition('type', 'admin');

        $j_adm = $this->join('admin_permission.user_id');

        $j_adm->addField('can_edit_users')->type('boolean');
        $j_adm->addField('can_edit_orders')->type('boolean');
    }
}

Now lets remember that we also used to stored Model_Admin inside $mongo database. The Mongo driver does not support joins and despite being really useful, the above code can introduce some confusion if our Model needs to be saved into different databases.

Fortunately when we create Model instance, we use add() method and it also links $model->connection property to $db. All we have to do now is to update our init() method:

class Model_Admin extends Model_User
{
    function init() {
        parent::init();

        $this->addCondition('type', 'admin');

        if ($this->connection->supports('join')) {
            $j_adm = $this->join('admin_permission.user_id');
        }else{
            $j_adm = $this;
        }

        $j_adm->addField('can_edit_users')->type('boolean');
        $j_adm->addField('can_edit_orders')->type('boolean');
    }
}

Now our model will create "join" only in the cases when connection claims to have "join" support. Lets look at the "Model_Client" model now and see what we can do there:

class Model_Client extends Model_User
{
    function init() {
        parent::init();

        $this->addCondition('type', 'client');

        $this->hasMany('Order');
        $this->hasMany('Order_Completed');

        if ($this->connection->supports('expressions')) {
            $this->addExpression('completed_orders')
                ->set($this->ref('Order_Completed')->count());
        } else {
            $this->addField('completed_orders');
        }
    }
}

Now we have a way to use Model_Client with databases that do not support expressions.

Lets modify Model_Order so that it would work with non-sql database:

class Model_Order extends atk4\data\Model
{
    public $table='order';
    function init() {
        parent::init();

        $this->hasMany('Order_Line');
        $this->hasOne('Client');

        if ($this->connection->supports('expressions')) {
            $this->addExpression('amount')
                ->set($this->ref('Order_Line')->sum('amount'));
        } else {
            $this->addField('amount');
        }
    }
    function complete() {
        $this['isCompleted']=true;
        $this->saveAndUnload();

        if (!$this->connection->supports('expressions')) {
            $this->ref('Client')->incr('completed_orders');
        }
    }
}

Finally, I want to point out that Models support wide range of hooks (some of them may be called by SQL drivers only):

  • beforeLoad
  • afterLoad
  • buildLoadQuery
  • beforeSave
  • afterSave
  • buildUpdateQuery
  • buildInsertQuery
  • beforeUpdate
  • afterUpdate
  • beforeInsert
  • afterInsert

When you call join() it will add some bindings, for example beforeInsert() it will insert record into joined table and will link up the IDs. It will also hook on buildLoadQuery and add necessary join there. This logic heavily relies on DSQL library to make sure that multiple joins do not end up messing up with other expressions or your custom code.

And since we started talking about query building - there are ways how you can [convert your Business Models into SQL](Derived Queries).