Cache ORM Phalcon

Bab Cache ORM Phalcon ini menjelaskan implementasi cache untuk meningkatkan kinerja ketika dibutuhkan. Framework ini memberi Anda alat untuk menerapkan cache di mana menurut Anda sesuai dengan arsitektur aplikasi Anda.

Setiap aplikasi itu berbeda, kita bisa memiliki model yang datanya sering berubah dan ada jg yang jarang berubah. Mengakses sistem database sering salah satu hambatan yang paling umum dalam hal kinerja. Hal ini disebabkan kompleksitas proses koneksi / komunikasi yang harus dilakukan PHP di setiap request untuk mendapatkan data dari database. Oleh karena itu, jika kita ingin mencapai kinerja yang baik kita perlu menambahkan beberapa lapisan cache dimana aplikasi akan membutuhkannya. 

Cache Resultset 

Sebuah teknik yg baik untuk menghindari akses berkelanjutan ke database yaitu untuk cache resultset/data yang jarang berubah menggunakan sistem dengan akses yang lebih cepat (biasanya memory).

Ketika Phalcon \ MVC \ Model membutuhkan layanan untuk cache resultsets , maka akan meminta ke Dependency Injector Container dengan nama “modelsCache”.

Sebagaimana Phalcon menyediakan komponen untuk men-cache setiap jenis data, kami akan menjelaskan bagaimana untuk mengintegrasikannya dengan Model. Pertama, Anda harus mendaftar sebagai layanan dalam wadah layanan:

<?php

//Set the models cache service
$di->set('modelsCache', function() {

    //Cache data for one day by default
    $frontCache = new \Phalcon\Cache\Frontend\Data(array(
        "lifetime" => 86400
    ));

    //Memcached connection settings
    $cache = new \Phalcon\Cache\Backend\Memcache($frontCache, array(
        "host" => "localhost",
        "port" => "11211"
    ));

    return $cache;
});

Anda memiliki kontrol penuh dalam menciptakan dan menyesuaikan cache sebelum digunakan dengan mendaftarkan layanan sebagai fungsi anonim. Setelah setup cache didefinisikan dengan benar Anda bisa cache resultsets sebagai berikut:

<?php

// Get products without caching
$products = Products::find();

// Just cache the resultset. The cache will expire in 1 hour (3600 seconds)
$products = Products::find(array(
    "cache" => array("key" => "my-cache")
));

// Cache the resultset for only for 5 minutes
$products = Products::find(array(
    "cache" => array("key" => "my-cache", "lifetime" => 300)
));

// Using a custom cache
$products = Products::find(array("cache" => $myCache));

Cache dapat juga diterapkan pada resultsets yg menggunakan relasi:

<?php

// Query some post
$post = Post::findFirst();

// Get comments related to a post, also cache it
$comments = $post->getComments(array(
    "cache" => array("key" => "my-key")
));

// Get comments related to a post, setting lifetime
$comments = $post->getComments(array(
    "cache" => array("key" => "my-key", "lifetime" => 3600)
));

Ketika cache resultset kadaluarsa  (invalidated), Anda dapat menghapusnya dari cache dengan menggunakan key yg telah ditentukan sebelumnya.

Perhatikan bahwa tidak semua resultsets harus di-cache. Hasil yang berubah sangat sering tidak harus di-cache karena mereka invalid/kadaluarsa sangat cepat dan cache justru berdampak pada kinerja. Sebagai tambahan, dataset besar yang tidak sering berubah bisa di-cache, tapi itu adalah keputusan pengembang berdasarkan pertimbangan mekanisme cache yang tersedia dan bagaimana dampak kinerja jika hanya mengambil data seperti biasa.

Overide find / findFirst 

Seperti yang terlihat di atas, metode ini tersedia dalam model yang mewarisi Phalcon \ MVC \ Model :

<?php

class Robots extends Phalcon\Mvc\Model
{

    public static function find($parameters=null)
    {
        return parent::find($parameters);
    }

    public static function findFirst($parameters=null)
    {
        return parent::findFirst($parameters);
    }

}

Dengan melakukan ini, Anda mencegat semua panggilan ke metode ini, cara ini, Anda dapat menambahkan lapisan cache atau menjalankan query jika tidak ada Cache. Sebagai contoh, implementasi cache yang sangat dasar, menggunakan properti statis untuk menghindari record akan di-query beberapa kali dalam permintaan yang sama:

<?php

class Robots extends Phalcon\Mvc\Model
{

    protected static $_cache = array();

    /**
     * Implement a method that returns a string key based
     * on the query parameters
     */
    protected static function _createKey($parameters)
    {
        $uniqueKey = array();
        foreach ($parameters as $key => $value) {
            if (is_scalar($value)) {
                $uniqueKey[] = $key . ':' . $value;
            } else {
                if (is_array($value)) {
                    $uniqueKey[] = $key . ':[' . self::_createKey($value) .']';
                }
            }
        }
        return join(',', $uniqueKey);
    }

    public static function find($parameters=null)
    {

        //Create an unique key based on the parameters
        $key = self::_createKey($parameters);

        if (!isset(self::$_cache[$key])) {
            //Store the result in the memory cache
            self::$_cache[$key] = parent::find($parameters);
        }

        //Return the result in the cache
        return self::$_cache[$key];
    }

    public static function findFirst($parameters=null)
    {
        // ...
    }

}

Akses ke database beberapa kali lebih lambat daripada mencari key cache, Anda bebas menerapkan strategi generate kunci dianggap lebih baik untuk kebutuhan Anda. Perhatikan bahwa kunci yang baik menghindari tabrakan sebanyak mungkin.

Dalam contoh di atas, kita menggunakan cache dalam memori, hal ini berguna sebagai cache tingkat pertama. Setelah kita memiliki memori cache, kita dapat menerapkan lapisan Cache tingkat kedua seperti APC / XCache atau database NoSQL:

<?php

public static function find($parameters=null)
{

    //Create an unique key based on the parameters
    $key = self::_createKey($parameters);

    if (!isset(self::$_cache[$key])) {

        //We're using APC as second cache
        if (apc_exists($key)) {

            $data = apc_fetch($key);

            //Store the result in the memory cache
            self::$_cache[$key] = $data;

            return $data;
        }

        //There are no memory or apc cache
        $data = parent::find($parameters);

        //Store the result in the memory cache
        self::$_cache[$key] = $data;

        //Store the result in APC
        apc_store($key, $data);

        return $data;
    }

    //Return the result in the cache
    return self::$_cache[$key];
}

Ini akan memberikan Anda kontrol penuh pada bagaimana cache harus dilaksanakan untuk masing-masing model, jika strategi ini sering dipakai untuk beberapa model Anda dapat membuat base class untuk semua:

<?php

class CacheableModel extends Phalcon\Mvc\Model
{

    protected static function _createKey($parameters)
    {
        // .. create a cache key based on the parameters
    }

    public static function find($parameters=null)
    {
        //.. custom caching strategy
    }

    public static function findFirst($parameters=null)
    {
        //.. custom caching strategy
    }
}

Kemudian gunakan class ini sebagai dasar class untuk setiap ‘Cacheable’ model:

<?php

class Robots extends CacheableModel
{

}

Memaksa Cache ORM Phalcon 

Sebelumnya kita melihat bagaimana Phalcon \ MVC \ Model memiliki built-in integrasi dengan komponen cache yang disediakan oleh framework. Agar record/resultset  disimpan di cache kita memberikan key ‘cache’ dalam array parameter:

<?php

// Cache the resultset for only for 5 minutes
$products = Products::find(array(
    "cache" => array("key" => "my-cache", "lifetime" => 300)
));

Ini memberi kita kebebasan untuk cache query tertentu, namun jika kita ingin cache secara global setiap query dilakukan melalui model, kita dapat mengganti metode find / FindFirst untuk memaksa setiap query untuk di-cache:

<?php

class Robots extends Phalcon\Mvc\Model
{

    protected static function _createKey($parameters)
    {
        // .. create a cache key based on the parameters
    }

    public static function find($parameters=null)
    {

        //Convert the parameters to an array
        if (!is_array($parameters)) {
            $parameters = array($parameters);
        }

        //Check if a cache key wasn't passed
        //and create the cache parameters
        if (!isset($parameters['cache'])) {
            $parameters['cache'] = array(
                "key" => self::_createKey($parameters),
                "lifetime" => 300
            );
        }

        return parent::find($parameters);
    }

    public static function findFirst($parameters=null)
    {
        //...
    }

}

Cache Query PHQL 

Semua query di ORM, tidak peduli seberapa tinggi tingkat sintaks yang kita digunakan untuk membuatnya tetap ditangani secara internal menggunakan PHQL. Bahasa ini memberi Anda lebih banyak kebebasan untuk membuat segala macam query. Tentu saja query ini dapat di-cache:

<?php

$phql = "SELECT * FROM Cars WHERE name = :name:";

$query = $this->modelsManager->createQuery($phql);

$query->cache(array(
    "key" => "cars-by-name",
    "lifetime" => 300
));

$cars = $query->execute(array(
    'name' => 'Audi'
));

Jika Anda tidak ingin menggunakan cache implisit dan hanya menyimpan resulset ke backend cache favorit Anda:

<?php

$phql = "SELECT * FROM Cars WHERE name = :name:";

$cars = $this->modelsManager->executeQuery($phql, array(
    'name' => 'Audi'
));

apc_store('my-cars', $cars);

Cache Berdasarkan Kondisi 

Dalam skenario ini, cache diimplementasikan kondisional sesuai dengan kondisi saat ini yang diterima. Menurut rentang di mana primary terletak kita memilih cache backend yang berbeda:

Jenis Cache Backend
1-10.000 mongo1
10000-20000 mongo2
> 20000 mongo3

Cara termudah adalah menambahkan metode statis pada model untuk memilih cache yang tepat untuk digunakan:

<?php

class Robots extends \Phalcon\Mvc\Model
{

    public static function queryCache($initial, $final)
    {
        if ($initial >= 1 && $final < 10000) {
            return self::find(array(
                'id >= ' . $initial . ' AND id <= '.$final,
                'cache' => array('service' => 'mongo1')
            ));
        }
        if ($initial >= 10000 && $final <= 20000) {
            return self::find(array(
                'id >= ' . $initial . ' AND id <= '.$final,
                'cache' => array('service' => 'mongo2')
            ));
        }
        if ($initial > 20000) {
            return self::find(array(
                'id >= ' . $initial,
                'cache' => array('service' => 'mongo3')
            ));
        }
    }

}

Pendekatan ini dapat memecahkan masalah, namun, jika kita ingin menambahkan parameter lain spt urutan atau kondisi, kita harus membuat metode yang lebih rumit. Selain itu, metode ini tidak bekerja jika data yang diperoleh dengan menggunakan record terkait atau find / FindFirst:

<?php

$robots = Robots::find('id < 1000');
$robots = Robots::find('id > 100 AND type = "A"');
$robots = Robots::find('(id > 100 AND type = "A") AND id < 2000');

$robots = Robots::find(array(
    '(id > ?0 AND type = "A") AND id < ?1',
    'bind' => array(100, 2000),
    'order' => 'type'
));

Untuk mencapai hal ini kita perlu untuk mencegat  intermediate representation (IR) yang dihasilkan oleh parser PHQL dan kemudian cache yg dikehendaki:

Yang pertama adalah membuat builder kustom, sehingga kita dapat menghasilkan query yg benar-benar disesuaikan:

<?php

class CustomQueryBuilder extends Phalcon\Mvc\Model\Query\Builder
{

    public function getQuery()
    {
        $query = new CustomQuery($this->getPhql());
        $query->setDI($this->getDI());
        return $query;
    }

}

Alih-alih langsung mengembalikan Phalcon\MVC\Model\Query, builder mengembalikan instance CustomQuery, class ini terlihat seperti:

<?php

class CustomQuery extends Phalcon\Mvc\Model\Query
{

    /**
     * The execute method is overridden
     */
    public function execute($params=null, $types=null)
    {
        //Parse the intermediate representation for the SELECT
        $ir = $this->parse();

        //Check if the query has conditions
        if (isset($ir['where'])) {

            //The fields in the conditions can have any order
            //We need to recursively check the conditions tree
            //to find the info we're looking for
            $visitor = new CustomNodeVisitor();

            //Recursively visits the nodes
            $visitor->visit($ir['where']);

            $initial = $visitor->getInitial();
            $final = $visitor->getFinal();

            //Select the cache according to the range
            //...

            //Check if the cache has data
            //...
        }

        //Execute the query
        $result = $this->_executeSelect($ir, $params, $types);

        //cache the result
        //...

        return $result;
    }

}

Menerapkan helper (CustomNodeVisitor) yang secara rekursif memeriksa kondisi mencari kolom yang beritahu kita berbagai kemungkinan yang akan digunakan dalam cache:

<?php

class CustomNodeVisitor
{

    protected $_initial = 0;

    protected $_final = 25000;

    public function visit($node)
    {
        switch ($node['type']) {

            case 'binary-op':

                $left = $this->visit($node['left']);
                $right = $this->visit($node['right']);
                if (!$left || !$right) {
                    return false;
                }

                if ($left=='id') {
                    if ($node['op'] == '>') {
                        $this->_initial = $right;
                    }
                    if ($node['op'] == '=') {
                        $this->_initial = $right;
                    }
                    if ($node['op'] == '>=')    {
                        $this->_initial = $right;
                    }
                    if ($node['op'] == '<') {
                        $this->_final = $right;
                    }
                    if ($node['op'] == '<=')    {
                        $this->_final = $right;
                    }
                }
                break;

            case 'qualified':
                if ($node['name'] == 'id') {
                    return 'id';
                }
                break;

            case 'literal':
                return $node['value'];

            default:
                return false;
        }
    }

    public function getInitial()
    {
        return $this->_initial;
    }

    public function getFinal()
    {
        return $this->_final;
    }
}

Akhirnya, kita dapat mengganti metode find dalam model Robot untuk menggunakan class kustom yg kita buat:

<?php

class Robots extends Phalcon\Mvc\Model
{
    public static function find($parameters=null)
    {

        if (!is_array($parameters)) {
            $parameters = array($parameters);
        }

        $builder = new CustomQueryBuilder($parameters);
        $builder->from(get_called_class());

        if (isset($parameters['bind'])) {
            return $builder->getQuery()->execute($parameters['bind']);
        } else {
            return $builder->getQuery()->execute();
        }

    }
}

Cache Perencanaan PHQL 

Sebagaimana sistem database yang paling modern, PHQL secara internal men-cache rencana eksekusi (pernyataan/query yg telah disiapkan), jika pernyataan yang sama dieksekusi beberapa kali PHQL menggunakan kembali rencana yang telah dihasilkan sebelumnya u/ meningkatkan kinerja, agar pengembang mengambil keuntungan lebih baik dari ini sangat dianjurkan membangun semua pernyataan SQL Anda melewati variabel parameter sebagai bound parameter:

<?php

for ($i = 1; $i <= 10; $i++) {

    $phql = "SELECT * FROM Store\Robots WHERE id = " . $i;
    $robots = $this->modelsManager->executeQuery($phql);

    //...
}

Dalam contoh di atas, sepuluh rencana yang dihasilkan meningkatkan penggunaan memori dan pengolahan dalam aplikasi. Menulis ulang kode untuk mengambil keuntungan dari parameter terikat mengurangi pengolahan baik oleh ORM dan sistem database:

<?php

$phql = "SELECT * FROM Store\Robots WHERE id = ?0";

for ($i = 1; $i <= 10; $i++) {

    $robots = $this->modelsManager->executeQuery($phql, array($i));

    //...
}

Kinerja dapat juga ditingkatkan dgn menggunakan kembali query PHQL:

<?php

$phql = "SELECT * FROM Store\Robots WHERE id = ?0";
$query = $this->modelsManager->createQuery($phql);

for ($i = 1; $i <= 10; $i++) {

    $robots = $query->execute($phql, array($i));

    //...
}

Rencana eksekusi untuk query yang melibatkan prepared statement yg juga di-cache oleh sebagian besar sistem database, mengurangi waktu eksekusi keseluruhan, juga melindungi aplikasi Anda terhadap SQL Injection .

 

Terjemahan dari Cache Pada ORM Phalcon
http://docs.phalconphp.com/en/latest/reference/models-cache.html