标签归档:ORM

ORM – illuminate/database组件

{
    "config": {
        "preferred-install": "dist",
        "secure-http": false
    },
    "repositories": [
        {"type": "composer", "url": "http://packagist.phpcomposer.com"},
        {"packagist": false}
    ],
    "require": {
        "illuminate/database": "^5.1"
    }
}

php composer.phar install
# OR
php composer.phar require illuminate/database

使用:

<?php
if(file_exists('vendor/autoload.php')){
    $loader = include '/vendor/autoload.php';
}else{
    exit("Autoload Failed. ");
}

// Capsule跟Laravel的DB Facade用法一样
use Illuminate\Database\Capsule\Manager as Capsule;

$capsule = new \Illuminate\Database\Capsule\Manager;
// 添加链接
// 注意:collation务必正确指定,不会根据charset自定设置
$capsule->addConnection([
    'driver'    => 'mysql',
    'host'      => 'localhost',
    'database'  => 'test',
    'username'  => 'root',
    'password'  => '',
    'charset'   => 'utf8',
    'collation' => 'utf8_general_ci',
    'prefix'    => '',
]);
// 这样就可以使用Capsule::xxx这样的方法
$capsule->setAsGlobal();
// 启动ORM支持
$capsule->bootEloquent();
//
$demo = Capsule::table('datatables_demo')->where('id', '>', 10)->take(5)->get();
//print_r($demo);

// ORM测试
class User extends \Illuminate\Database\Eloquent\Model {
	protected $table = 'user';
}
$users = User::where('id', '>', 0)->get();
print_r($users->toArray());

想知道$capsule->setAsGlobal();干了啥,看如下例子:

<?php
if(file_exists('vendor/autoload.php')){
    $loader = include '/vendor/autoload.php';
}else{
    exit("Autoload Failed. ");
}

// Capsule跟Laravel的DB Facade用法一样
use Illuminate\Database\Capsule\Manager as DB;

$db = new DB;
// 添加链接
// 注意:collation务必正确指定,不会根据charset自定设置
$db->addConnection([
    'driver'    => 'mysql',
    'host'      => 'localhost',
    'database'  => 'test',
    'username'  => 'root',
    'password'  => '',
    'charset'   => 'utf8',
    'collation' => 'utf8_general_ci',
    'prefix'    => '',
]);
// 这样就可以使用Capsule::xxx这样的方法
$db->setAsGlobal();
// 启动ORM支持
$db->bootEloquent();
//
$demo = DB::table('datatables_demo')->where('id', '>', 10)->take(5)->get();
//print_r($demo);

这样就跟Laravel中的DB Facade一致了。不过这里的DB并不是一个Facade。实际上,use Illuminate\Database\Capsule\Manager as DB是把Illuminate\Database\Capsule\Manager类设置了别名叫DB,自然,能通过Illuminate\Database\Capsule\Manager类访问的静态方法,也可以通过DB来方法,比如Illuminate\Database\Capsule\Manager有table()方法,那么DB::table()自然是可以的。

Illuminate\Database\Capsule\Manager类中:

    public static function __callStatic($method, $parameters)
    {
        return call_user_func_array([static::connection(), $method], $parameters);
    }

    public static function connection($connection = null)
    {
        return static::$instance->getConnection($connection);
    }

当访问类中不存在的静态方法时,_callStatic()方法会被触发,而它实际是调用数据库链接的静态方法,而数据库链接是通过调用static::$instance实例的getConnection()获取的,setAsGlobal()方法就是把当前实例赋值给静态变量static::$instance,这样就实现了在静态方法中调用实例方法。

下面跟踪一下DB::table(),大体看下流程:

// 调用table()方法,实际调用了static::$instance->connection($connection)的table()方法
    public static function table($table, $connection = null)
    {
        return static::$instance->connection($connection)->table($table);
    }
// static::$instance->connection($connection)有调用了getConnection
    public static function connection($connection = null)
    {
        return static::$instance->getConnection($connection);
    }
// 调用了$this->manager的connection($name)方法
// $this->manager是一个Illuminate\Database\DatabaseManager实例,在Illuminate\Database\Capsule\Manager构造是被创建
// 这个实例就是static::$instance
    public function getConnection($name = null)
    {
        return $this->manager->connection($name);
    }

// $this->manager->connection($name)方法中调用makeConnection()来更加不同的配置生成具体的链接
// 这个链接生成的过程会调用具体的Connector建立连接器
// Manager的connections数据保存的是已经链接上数据库的具体链接对象(下次就可以直接使用)
// 所以,大部分操作函数都是直接或间接对连接对象的方法的调用
    public function connection($name = null)
    {
        list($name, $type) = $this->parseConnectionName($name);

        // If we haven't created this connection, we'll create it based on the config
        // provided in the application. Once we've created the connections we will
        // set the "fetch mode" for PDO which determines the query return types.
        if (! isset($this->connections[$name])) {
            $connection = $this->makeConnection($name);

            $this->setPdoForType($connection, $type);

            $this->connections[$name] = $this->prepare($connection);
        }

        return $this->connections[$name];
    }

简单来说,Illuminate\Database\Capsule\Manager的大部分静态方法实际是调用了Illuminate\Database\DatabaseManager实例中管理的Connection对象,Connection对象的具体的链接又是由Connector来处理的。Connection类就是最终参考。具体来说就是Illuminate\Database\MySqlConnection,而它继承自Illuminate\Database\Connection,比如table()方法,statement()方法,都在这里定义。比如getSchemaBuilder()会产生一个结构构建器,而query()方法会产生一个查询构建器。ORM的实现自然是要依赖具体的Connection对象的。

最后,我们还关心一个掉线重连的方法:最终的产生的SQL会由run()方法来运行:

    protected function run($query, $bindings, Closure $callback)
    {
        $this->reconnectIfMissingConnection();

        $start = microtime(true);

        // Here we will run this query. If an exception occurs we'll determine if it was
        // caused by a connection that has been lost. If that is the cause, we'll try
        // to re-establish connection and re-run the query with a fresh connection.
        try {
            $result = $this->runQueryCallback($query, $bindings, $callback);
        } catch (QueryException $e) {
            if ($this->transactions >= 1) {
                throw $e;
            }

            $result = $this->tryAgainIfCausedByLostConnection(
                $e, $query, $bindings, $callback
            );
        }

        // Once we have run the query we will calculate the time that it took to run and
        // then log the query, bindings, and execution time so we will report them on
        // the event that the developer needs them. We'll log time in milliseconds.
        $time = $this->getElapsedTime($start);

        $this->logQuery($query, $bindings, $time);

        return $result;
    }

首先调用reconnectIfMissingConnection()来判断是否已经掉线,如果掉线则重新链接。然后运行SQL,运行是如果抛出异常,判断是否是因为掉线而抛出的异常,如果是则重连后再次运行SQL。这里有一个双重保险,在队列监听器中,这个就特别有用了。

Laravel 的ORM使用

容易混淆的地方:

        /*
        $item = Item::withTrashed()->where('sku','YW10046');
        
        $item->first()
        $item->firstOrFail()
        $item->firstOrNew()                 X builder不存在   
        $item->firstOrCreate()              X builder不存在 
        
        $item->find($id);
        $item->findOrFail($id)
        $item->findOrNew($id)               X builder不存在
        $item->findOrCreate($id)            XXX
        
        Item::first()                       实际调用builder的first()
        Item::firstOrFail()                 实际调用builder的firstOrFail()
        Item::firstOrNew($attributes);      
        Item::firstOrCreate($attributes);   
        
        
        Item::find($id)                     实际调用builder的find()
        Item::findOrFail($id)               实际调用builder的findOrFail()
        Item::findOrNew($id)     
        Item::findOrCreate($id)             XXX 
        */

方法findOrCreate($id)是怎么都不存在的。对于一个builder,只有find或findOrFail,first或firstOrFail。对于一个模型,在find或findOrFail,first或firstOrFail的基础上(实际也是间接调用builder的这些方法),多了findOrNew($id),firstOrNew($attributes)和firstOrCreate($attributes),三个方法,这三个方法都应该返回模型实例,firstOrCreate()如果不存在会先插入。

//Illuminate\Database\Eloquent\Model
    public static function findOrNew($id, $columns = ['*'])
    {
        if (! is_null($model = static::find($id, $columns))) {
            return $model;
        }

        return new static;
    }

    public static function firstOrCreate(array $attributes)
    {
        if (! is_null($instance = static::where($attributes)->first())) {
            return $instance;
        }

        return static::create($attributes);
    }

    public static function firstOrNew(array $attributes)
    {
        if (! is_null($instance = static::where($attributes)->first())) {
            return $instance;
        }

        return new static($attributes);
    }

可以看到,多出来的这个三个方法,实际是find()和first()方法再次封装搞出来的。firstOrCreate和firstOrNew接收一个数组,实际传递到builder的where方法,where方法如果第一参数传递的是一个数组,比如[‘name’=>’xxx’,’sku’=>’yyyy’],最终变成where name=’xxx’ AND sku=’yyyy’,这个比较死板了。接下来再看看builder的first和find方法:

    public function first($columns = ['*'])
    {
        $results = $this->take(1)->get($columns);

        return count($results) > 0 ? reset($results) : null;
    }
    public function find($id, $columns = ['*'])
    {
        return $this->where('id', '=', $id)->first($columns);
    }

你可以看到,真正的方法只有first。static::where()这种调用,first会返回模型实例。

所有的 Eloquent 模型都继承于 Illuminate\Database\Eloquent\Model(有预置一个别名Eloquent => Illuminate\Database\Eloquent\Model::class)。

// 定义模型
class User extends Model {}

// 通过命令生成(默认对应小写复数的数据表)
php artisan make:model User

// 可以手工指定(常见)
class User extends Model {
    protected $table = 'my_users';
}
 

Eloquent 也会假设每个数据库表都有一个字段名称为 id 的主键。可以在类里定义primaryKey属性来重写。同样的,也可以定义connection属性,指定模型连接到指定的数据库连接(看起来,链接是针对数据库的)。

注意在默认情况下,在数据库表里需要有 updated_at 和 created_at 两个字段。如果您不想设定或自动更新这两个字段,则将类里的 $timestamps 属性设为 false即可。

class User extends Model {
    protected $table = 'my_users';
    protected $primaryKey = 'xxx';
    protected $connection = 'xx_db';
    protected $timestamps = false;
}
// 取所有数据
$users = User::all();

// 取id=1的数据
$user = User::find(1);
var_dump($user->name);

// 所有查询构造器里的方法,查询 Eloquent 模型时也可以使用。

// 根据主键取出一条数据或抛出异常(找不到不用抛异常吧)
// Illuminate\Database\Eloquent\ModelNotFoundException
$model = User::findOrFail(1);
$model = User::where('votes', '>', 100)->firstOrFail();

// 过滤查询,跟查询构造器一模一样?
$users = User::where('votes', '>', 100)->take(10)->get();
foreach ($users as $user)
{
    var_dump($user->name);
}

// Eloquent 聚合查询
$count = User::where('votes', '>', 100)->count();
// 如果where用得不爽,还可以这样
$users = User::whereRaw('age > ? and votes = 100', [25])->get();

// 拆分查询
User::chunk(200, function($users)
{
    foreach ($users as $user)
    {
        //
    }
});

// 指定查询时连接数据库(链接 和 数据库链接还是有点差别)
$user = User::on('connection-name')->find(1);
$user = User::onWriteConnection()->find(1);

在建立一个新的模型时,您把属性以数组的方式传入模型的构造方法,这些属性值会经由批量赋值存成模型数据。所有的 Eloquent 模型默认会阻止批量赋值(令人无语)。

//定义模型 Fillable 属性,就是那些属性可以填充
class User extends Model {
    protected $fillable = ['first_name', 'last_name', 'email'];

}

//定义模型 Guarded 属性(不能搞的属性)
class User extends Model {
    protected $guarded = ['id', 'password'];
    // 阻止所有
    protected $guarded = ['*'];
}

使用 guarded 时, Input::get() 或任何用户可以控制的未过滤数据,永远不应该传入 save 或 update 方法。

$user = new User;
$user->name = 'John';
$user->save();

通常 Eloquent 模型主键值会自动递增。但是您若想自定义主键,将 incrementing 属性设成 false。(通常没有多大必要)

也可以使用 create 方法存入新的模型数据,新增完后会返回新增的模型实例。但是在新增前,需要先在模型类里设定好 fillable 或 guarded 属性,因为 Eloquent 默认会防止批量赋值。(默认定义guarded等于空数据,搞掉这个恶心巴拉的东西)

在新模型数据被储存或新增后,若模型有自动递增主键,可以从对象取得 id 属性值:

$insertedId = $user->id;
// 在数据库中建立一个新的用户...
$user = User::create(['name' => 'John']);

// 以属性找用户,若没有则新增并取得新的实例...
$user = User::firstOrCreate(['name' => 'John']);

// 以属性找用户,若没有则建立新的实例...
$user = User::firstOrNew(['name' => 'John']);

注意看firstOrCreate和firstOrNew,firstOrNew如果查询不到,就来一个新模型而已,而firstOrCreate是找不到,插入一条记录,然后返回填充模型,再返回模型实例。所以,firstOrCreate总是会有id的。

// 取出数据,修改,保存
$user = User::find(1);
$user->email = 'john@foo.com';
$user->save();

// 存储关联数据,需要这样更新
$user->push();
// 批量更新
$affectedRows = User::where('votes', '>', 100)->update(['status' => 2]);
// 删除
$user = User::find(1);
$user->delete();

// 这个与上一个不同在于,它应该是直接发送删除指令
User::destroy(1);
User::destroy([1, 2, 3]);
User::destroy(1, 2, 3);

//
$affectedRows = User::where('votes', '>', 100)->delete();
// 只更新模型的时间戳
$user->touch();

通过软删除方式删除了一个模型后,模型中的数据并不是真的从数据库被移除。而是会设定 deleted_at时间戳。要让模型使用软删除功能,只要在模型类里加入 SoftDeletingTrait 即可:

use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Model {

    use SoftDeletes;

    protected $dates = ['deleted_at'];

}

要加入 deleted_at 字段到数据库表,可以在迁移文件里使用 softDeletes 方法:

$table->softDeletes();

软删除视乎很流行…。这样子之后,所有的删除都只是把删除的时间添加上去而已。

强制查询软删除数据

$users = User::withTrashed()->where('account_id', 1)->get();

// 关联查询,posts()是用户的所有post
$user->posts()->withTrashed()->get();

// 只查询被删的
$users = User::onlyTrashed()->where('account_id', 1)->get();

// 恢复
$user->restore();
User::withTrashed()->where('account_id', 1)->restore();
$user->posts()->restore();

// 真删除
$user->forceDelete();
$user->posts()->forceDelete();

// 确认被软删
if ($user->trashed())
{
    //
}

// 关闭自动更新时间戳
class User extends Model {

    protected $table = 'users';

    public $timestamps = false;

}

//自定义时间戳格式,可以在模型类里重写 getDateFormat 方法
class User extends Model {

    protected function getDateFormat()
    {
        return 'U';
    }

}

// 这个搞法,看看就好了
class User extends Model {

    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

    public function scopeWomen($query)
    {
        return $query->whereGender('W');
    }

}
$users = User::popular()->women()->orderBy('created_at')->get();

class User extends Model {

    public function scopeOfType($query, $type)
    {
        return $query->whereType($type);
    }

}
$users = User::ofType('member')->get();

Global Scopes
这个是软删除实现的原理。(。。。)

关联
这个内容,作为一个ORM,必是不可少的了。不过需要注意,千万别滥用。

////////一对一
class User extends Model {

    public function phone()
    {
        return $this->hasOne('App\Phone');
    }

}
// 这样就可以这样用(所谓的动态属性),实际是发起一条新查询
$phone = User::find(1)->phone;

// 指定关联的Key,注意,这个Key并不要求真的存在
return $this->hasOne('App\Phone', 'foreign_key');
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');

// 定义相对的关联
class Phone extends Model {

    public function user()
    {
        return $this->belongsTo('App\User', 'local_key', 'parent_key');
    }

}

////////一对多(本质上,跟一对一没有太大差别)
class Post extends Model {

    public function comments()
    {
        return $this->hasMany('App\Comment', 'foreign_key', 'local_key');
    }

}
$comments = Post::find(1)->comments;
// 建议这样干
$comments = Post::find(1)->comments()->where('title', '=', 'foo')->first();

// 定义相对的关联
class Comment extends Model {

    public function post()
    {
        return $this->belongsTo('App\Post');
    }

}

//////// 多对多
// users 和 roles 使用role_user作为中间表
class User extends Model {

    public function roles()
    {
        // return $this->belongsToMany('App\Role');
	// 第二字段指定中间表,有默认值。第三第四指定关联字段
	return $this->belongsToMany('App\Role', 'user_roles', 'user_id', 'foo_id');
    }

}

class Role extends Model {

    public function users()
    {
        return $this->belongsToMany('App\User');
    }

}
// 注意,中间表也是要对应一个模型的


////////Has Many Through 远层一对多关联 ...

////////多态关联

////////多态的多对多关联

关联查询(必须先关联)

$posts = Post::has('comments')->get();
$posts = Post::has('comments', '>=', 3)->get();
$posts = Post::has('comments.votes')->get();
$posts = Post::whereHas('comments', function($q)
{
    $q->where('content', 'like', 'foo%');

})->get();

预载入
这个内容讨论的就是ORM的缺点,以及如何补缺。

新增关联模型
更新上层时间戳
使用枢纽表
集合

$roles = User::find(1)->roles->toArray();
$roles = User::find(1)->roles->toJson();

$users = $users->filter(function($user)
{
    return $user->isAdmin();
});

获取器和修改器

日期转换器
可以通过重写模型的 getDates 方法,自定义哪个字段可以被自动转换,或甚至完全关闭这个转换:

public function getDates()
{
    return ['created_at'];
}

属性类型转换

/**
 * 需要被转换成基本类型的属性值。
 *
 * @var array
 */
protected $casts = [
    'is_admin' => 'boolean',
];

其它支持integer, real, float, double, string, boolean, object 和 array

模型事件
模型观察者
模型 URL 生成
转换成数组 / JSON

$user = User::with('roles')->first();
return $user->toArray();
return User::all()->toArray();
return User::find(1)->toJson();

这个章节确实是内容最多的。我们看到,相比其它的ORM,虽然没有那么庞大,但是提供了很多实用的功能,模型的数据没有和数据表的Schema对照进行类型绑定,个人觉得也没有必要,模型的数据类型自然是自己控制,再说去查询一次Schema本身就很恶。

PHP框架Phalcon 之 在ORM中缓存

Caching in the ORM 在ORM中缓存
Every application is different, we could have models whose data change frequently and others that rarely change. Accessing database systems is often one of the most common bottlenecks in terms of performance. This is due to the complex connection/communication processes that PHP must do in each request to obtain data from the database. Therefore, if we want to achieve good performance we need to add some layers of caching where the application requires it.
不经常改变的数据要缓存起来…

This chapter explains the possible points where it is possible to implement caching to improve performance. The framework gives you the tools to implement the cache where you demand of it according to the architecture of your application.
说它提供了缓存。

Caching Resultsets 缓存结果集
A well established technique to avoid the continuous access to the database is to cache resultsets that don’t change frequently using a system with faster access (usually memory).
把不经常改变的结果集缓存起来。

When Phalcon\Mvc\Model requires a service to cache resultsets, it will request it to the Dependency Injector Container with the convention name “modelsCache”.
当Phalcon\Mvc\Model要去缓存结果集,它会通过DI容器获取“modelsCache”(这个是缓存对象,复杂读取写入缓存)。

As Phalcon provides a component to cache any kind of data, we’ll explain how to integrate整合 it with Models. First, you must register it as a service in the services container:
首先要在容器中注册:

<?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;
});

You have complete control in creating and customizing the cache before being used by registering the service as an anonymous function. Once the cache setup is properly defined you could cache resultsets as follows:
可以通过匿名函数完全控制缓存对象的创建和定制。如下缓存结果集:

<?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));

Caching could be also applied to resultsets generated using relationships:
缓存也可以应用到由关系产生的结果集

<?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)
));

When a cached resultset needs to be invalidated, you can simply delete it from the cache using the previously specified key.
缓存无效,可以通过指定的key删除。

Note that not all resultsets must be cached. Results that change very frequently should not be cached since they are invalidated very quickly and caching in that case impacts performance. Additionally, large datasets that do not change frequently could be cached, but that is a decision that the developer has to make based on the available caching mechanism and whether the performance impact to simply retrieve that data in the first place is acceptable.(一段废话)

Overriding find/findFirst 重写find/findFirst
As seen above, these methods are available in models that inherit 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);
    }

}

By doing this, you’re intercepting all the calls to these methods, this way, you can add a cache layer or run the query if there is no cache. For example, a very basic cache implementation, uses a static property to avoid that a record would be queried several times in a same request:
重写这两个方法,添加缓存层:

<?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)
    {
        // ...
    }

}

Access the database is several times slower than calculate a cache key, you’re free in implement the key generation strategy you find better for your needs. Note that a good key avoids collisions as much as possible, this means that different keys returns unrelated records to the find parameters.

In the above example, we used a cache in memory, it is useful as a first level cache. Once we have the memory cache, we can implement a second level cache layer like APC/XCache or a NoSQL database:
实现第二层次缓存:

<?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];
}

This gives you full control on how the the caches must be implemented for each model, if this strategy is common to several models you can create a base class for all of them:
建立一个公共模型,然后让其它模型继承它,那么就可以使用自定义的缓存策略。

<?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
    }
}

Then use this class as base class for each ‘Cacheable’ model:

<?php

class Robots extends CacheableModel
{

}

Forcing Cache
这个内容跟以上内容重复。

Caching PHQL Queries PHQL查询缓存
All queries in the ORM, no matter how high level syntax we used to create them are handled internally using PHQL. This language gives you much more freedom to create all kinds of queries. Of course these queries can be cached:
内部都使用PHQL,这种查询可以被缓存:(PHQL先装换成IR,这里就是缓存这个IR)

<?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'
));

(注意,缓存对象务必要在DI中先注册)

If you don’t want to use the implicit cache just save the resulset into your favorite cache backend:

<?php

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

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

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

Reusable Related Records
Caching Related Records
Caching Related Records Recursively
Caching based on Conditions
Caching of PHQL planning

PHP框架Phalcon 之 模型 关系定义

Relationships between Models 模型关系
There are four types of relationships: one-on-one, one-to-many, many-to-one and many-to-many. The relationship may be unidirectional or bidirectional, and each can be simple (a one to one model) or more complex (a combination of models). The model manager manages foreign key constraints for these relationships, the definition of these helps referential integrity as well as easy and fast access of related records to a model. Through the implementation of relations, it is easy to access data in related models from each record in a uniform way.
有四种关系类型:one-on-one, one-to-many, many-to-one and many-to-many。这些关系可以是单向或双向,可以简单或复杂。模型管理器为这些关系管理外键约束,这些定义 引用完整性 更简单和快速访问相关记录到模型。通过这些关系的实现,可以使用统一的方式在相关模型中容易地获取来自每条记录的数据。

Unidirectional relationships 单向关系
Unidirectional relations are those that are generated in relation to one another but not vice versa反过来不行.

Bidirectional relations 双向关系
The bidirectional relations build relationships in both models and each model defines the inverse relationship of the other.

Defining relationships 定义关系
In Phalcon, relationships must be defined in the initialize() method of a model. 必须在模型的 initialize()方法中定义关系。The methods belongsTo(), hasOne(), hasMany() and hasManyToMany() define the relationship between one or more fields from the current model to fields in another model. Each of these methods requires 3 parameters: local fields, referenced model, referenced fields.(三个参数,本地字段,引用模型,引用字段)

Method Description
hasMany Defines a 1-n relationship
hasOne Defines a 1-1 relationship
belongsTo Defines a n-1 relationship
hasManyToMany Defines a n-n relationship

(如果某个模型被其它模型引用,就需要定义hasMany,就是所谓的一对多,同理,如果模型引用到另外模型,那么它就需要定义belongsTo,如果通过一个中间表引用到另外的模型,就需要定义hasManyToMany)

The following schema shows 3 tables whose relations will serve us as an example regarding relationships:

CREATE TABLE `robots` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(70) NOT NULL,
    `type` varchar(32) NOT NULL,
    `year` int(11) NOT NULL,
    PRIMARY KEY (`id`)
);

CREATE TABLE `robots_parts` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `robots_id` int(10) NOT NULL,
    `parts_id` int(10) NOT NULL,
    `created_at` DATE NOT NULL,
    PRIMARY KEY (`id`),
    KEY `robots_id` (`robots_id`),
    KEY `parts_id` (`parts_id`)
);

CREATE TABLE `parts` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(70) NOT NULL,
    PRIMARY KEY (`id`)
);

这三张表就是数据库里面常见的多对多关系,在数据库中只要维护中间表的引用即可。不过在这里就要复杂一些。
The model “Robots” has many “RobotsParts”.
The model “Parts” has many “RobotsParts”.
The model “RobotsParts” belongs to both “Robots” and “Parts” models as a many-to-one relation.
The model “Robots” has a relation many-to-many to “Parts” through “RobotsParts”
(被引用的是老子,它有多个仔,老子自然就是有多个仔,仔自然就属于老子,但是多到多就不好理解了,实际它是指要用中间表建立多对多关系,这些模型管理器才知道这些关系)

The models with their relations could be implemented as follows:

<?php

class Robots extends \Phalcon\Mvc\Model
{
    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany("id", "RobotsParts", "robots_id");
    }

}
<?php

class Parts extends \Phalcon\Mvc\Model
{

    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany("id", "RobotsParts", "parts_id");
    }

}
<?php

class RobotsParts extends \Phalcon\Mvc\Model
{

    public $id;

    public $robots_id;

    public $parts_id;

    public function initialize()
    {
        $this->belongsTo("robots_id", "Robots", "id");
        $this->belongsTo("parts_id", "Parts", "id");
    }

}

The first parameter indicates the field of the local model used in the relationship; the second indicates the name of the referenced model and the third the field name in the referenced model. You could also use arrays to define multiple fields in the relationship.
Many to many relationships require 3 models and define the attributes involved in the relationship:

<?php

class Robots extends \Phalcon\Mvc\Model
{
    public $id;

    public $name;

    public function initialize()
    {
        $this->hasManyToMany(
            "id",
            "RobotsParts",
            "robots_id", "parts_id",
            "Parts",
            "id"
        );
    }

}
//如果在Parts中定义这个多到多,看起来应该是可以的
<?php

class Parts extends \Phalcon\Mvc\Model
{

    public $id;

    public $name;

    public function initialize()
    {
	$this->hasManyToMany(
            "id",
            "RobotsParts",
            "parts_id", "robots_id ",
            "Robots",
            "id"
        );
	}
}

Taking advantage of relationships
When explicitly defining the relationships between models, it is easy to find related records for a particular record.

<?php

$robot = Robots::findFirst(30);
foreach ($robot->robotsParts as $robotPart) {
    echo $robotPart->parts->name, "\n";
}

优点先不说了,先看看产生的SQL吧

Wed, 03 Sep 14 09:06:05 +0800][INFO] SELECT `robots`.`id`, `robots`.`name`, `robots`.`type`, `robots`.`year` FROM `robots` WHERE `robots`.`id` = 30 LIMIT 1
[Wed, 03 Sep 14 09:06:05 +0800][INFO] SELECT `robots_parts`.`id`, `robots_parts`.`robots_id`, `robots_parts`.`parts_id`, `robots_parts`.`created_at` FROM `robots_parts` WHERE `robots_parts`.`robots_id` = :0
[Wed, 03 Sep 14 09:06:05 +0800][INFO] SELECT `parts`.`id`, `parts`.`name` FROM `parts` WHERE `parts`.`id` = :0 LIMIT 1
[Wed, 03 Sep 14 09:06:05 +0800][INFO] SELECT `parts`.`id`, `parts`.`name` FROM `parts` WHERE `parts`.`id` = :0 LIMIT 1
[Wed, 03 Sep 14 09:06:05 +0800][INFO] SELECT `parts`.`id`, `parts`.`name` FROM `parts` WHERE `parts`.`id` = :0 LIMIT 1

预计会产生联合查询,实际结果让人失望。代码是方便了,实际效率很差。从robot获取robot_part,在从robot_part获取part。

Phalcon uses the magic methods __set/__get/__call to store or retrieve related data using relationships.
By accessing an attribute with the same name as the relationship will retrieve all its related record(s).
Also, you can use a magic getter:

<?php

$robot = Robots::findFirst();
$robotsParts = $robot->getRobotsParts(); // all the related records in RobotsParts
$robotsParts = $robot->getRobotsParts(array('limit' => 5)); // passing parameters

If the called method has a “get” prefix Phalcon\Mvc\Model will return a findFirst()/find() result. The following example compares retrieving related results with using magic methods and without:

<?php

$robot = Robots::findFirst(2);

// Robots model has a 1-n (hasMany)
// relationship to RobotsParts then
$robotsParts = $robot->robotsParts;

// Only parts that match conditions
$robotsParts = $robot->getRobotsParts("created_at = '2012-03-15'");

// Or using bound parameters
$robotsParts = $robot->getRobotsParts(array(
    "created_at = :date:",
    "bind" => array("date" => "2012-03-15")
));

$robotPart = RobotsParts::findFirst(1);

// RobotsParts model has a n-1 (belongsTo)
// relationship to RobotsParts then
$robot = $robotPart->robots;

##########
##Getting related records manually:
<?php

$robot = Robots::findFirst(2);

// Robots model has a 1-n (hasMany)
// relationship to RobotsParts, then
$robotsParts = RobotsParts::find("robots_id = '" . $robot->id . "'");

// Only parts that match conditions
$robotsParts = RobotsParts::find(
    "robots_id = '" . $robot->id . "' AND created_at = '2012-03-15'"
);

$robotPart = RobotsParts::findFirst(1);

// RobotsParts model has a n-1 (belongsTo)
// relationship to RobotsParts then
$robot = Robots::findFirst("id = '" . $robotPart->robots_id . "'");

The prefix “get” is used to find()/findFirst() related records. Depending on the type of relation it will use ‘find’ or ‘findFirst’:

Type Description Implicit Method
Belongs-To Returns a model instance of the related record directly findFirst
Has-One Returns a model instance of the related record directly findFirst
Has-Many Returns a collection of model instances of the referenced model find
Has-Many-to-Many Returns a collection of model instances of the referenced model, it implicitly does ‘inner joins’ with the involved models (complex query)

You can also use “count” prefix to return an integer denoting the count of the related records:

<?php

$robot = Robots::findFirst(2);
echo "The robot has ", $robot->countRobotsParts(), " parts\n";

Aliasing Relationships 关系别名
To explain better how aliases work, let’s check the following example:
Table “robots_similar” has the function to define what robots are similar to others:

mysql> desc robots_similar;
+-------------------+------------------+------+-----+---------+----------------+
| Field             | Type             | Null | Key | Default | Extra          |
+-------------------+------------------+------+-----+---------+----------------+
| id                | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| robots_id         | int(10) unsigned | NO   | MUL | NULL    |                |
| similar_robots_id | int(10) unsigned | NO   |     | NULL    |                |
+-------------------+------------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

A model that maps this table and its relationships is the following:

<?php

class RobotsSimilar extends Phalcon\Mvc\Model
{

    public function initialize()
    {
        $this->belongsTo('robots_id', 'Robots', 'id');
        $this->belongsTo('similar_robots_id', 'Robots', 'id');
    }

}

Since both relations point to the same model (Robots), obtain the records related to the relationship could not be clear:

<?php

$robotsSimilar = RobotsSimilar::findFirst();

//Returns the related record based on the column (robots_id)
//Also as is a belongsTo it's only returning one record
//but the name 'getRobots' seems to imply that return more than one
$robot = $robotsSimilar->getRobots();

//but, how to get the related record based on the column (similar_robots_id)
//if both relationships have the same name?

The aliases allow us to rename both relationships to solve these problems:

<?php

class RobotsSimilar extends Phalcon\Mvc\Model
{

    public function initialize()
    {
        $this->belongsTo('robots_id', 'Robots', 'id', array(
            'alias' => 'Robot'
        ));
        $this->belongsTo('similar_robots_id', 'Robots', 'id', array(
            'alias' => 'SimilarRobot'
        ));
    }

}

With the aliasing we can get the related records easily:

<?php

$robotsSimilar = RobotsSimilar::findFirst();

//Returns the related record based on the column (robots_id)
$robot = $robotsSimilar->getRobot();
$robot = $robotsSimilar->robot;

//Returns the related record based on the column (similar_robots_id)
$similarRobot = $robotsSimilar->getSimilarRobot();
$similarRobot = $robotsSimilar->similarRobot;

Magic Getters vs. Explicit methods
Most IDEs and editors with auto-completion capabilities can not infer提示 the correct types when using magic getters, instead of use the magic getters you can optionally define those methods explicitly明确的 with the corresponding相应的 docblocks(DOC块) helping the IDE to produce a better auto-completion:

<?php

class Robots extends \Phalcon\Mvc\Model
{

    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany("id", "RobotsParts", "robots_id");
    }

    /**
     * Return the related "robots parts"
     *
     * @return \RobotsParts[]
     */
    public function getRobotsParts($parameters=null)
    {
        return $this->getRelated('RobotsParts', $parameters);
    }

}

Virtual Foreign Keys
By default, relationships do not act like database foreign keys, that is, if you try to insert/update a value without having a valid value in the referenced model, Phalcon will not produce a validation message. You can modify this behavior by adding a fourth parameter when defining a relationship.
默认定义的关系没有扮演类似数据库中的外键,如果插入或更新一个在引用模型中没有验证的值,Phalcon不会产生验证消息。你可以在定义关系时添加第四个参数修改这个行为。
The RobotsPart model can be changed to demonstrate this feature:

<?php

class RobotsParts extends \Phalcon\Mvc\Model
{

    public $id;

    public $robots_id;

    public $parts_id;

    public function initialize()
    {
        $this->belongsTo("robots_id", "Robots", "id", array(
            "foreignKey" => true
        ));

        $this->belongsTo("parts_id", "Parts", "id", array(
            "foreignKey" => array(
                "message" => "The part_id does not exist on the Parts model"
            )
        ));
    }

}

If you alter a belongsTo() relationship to act as foreign key, it will validate that the values inserted/updated on those fields have a valid value on the referenced model. 如果在belongsTo()中定义了外键,那么在插入或更新值时将在引用模型中验证值。 Similarly, if a hasMany()/hasOne() is altered it will validate that the records cannot be deleted if that record is used on a referenced model. 类似,如果hasMany()/hasOne()定义了外键,记录被删除时将检查记录是否被引用。
(belongsTo()/hasMany()/hasOne()之间是逻辑上有外键约束的,但是hasManyToMany()两个实体间被没有这个逻辑关系)

<?php

class Parts extends \Phalcon\Mvc\Model
{

    public function initialize()
    {
        $this->hasMany("id", "RobotsParts", "parts_id", array(
            "foreignKey" => array(
                "message" => "The part cannot be deleted because other robots are using it"
            )
        ));
    }

}

(这个被引用的表设置关联外键之后(严格来说对它不是外键约束,是外键关联),当记录被删除时将被阻止)
————————————————————————
为了更好理解这个阻止操作,使用如下代码测试:

class PushController extends \Phalcon\Mvc\Controller{
public function deletePartAction(){
$part = Parts::findFirst(7);
       	$ros = '';
      	foreach($part->robots as $robot){
        	$ros .= $robot->id."($robot->name) ";
      	}       
     	echo "The robots --> ".$ros." has the same part($part->id $part->name)\n\n";

       	if($part->delete() == false){ 
       		foreach($part->getMessages() as $m){ 
          		echo "Message: ", $m->getMessage(), "\n";
               	echo "Field:   ", $m->getField(), "\n";
             	echo "Type:    ", $m->getType(), "\n";
           	}       
      	}       
    	$this->view->disable()
	}
}

#########
##输出
The robots --> 31(new robot) 36(new robot)  has the same part(7 part_6)

Message: The part cannot be deleted because other robots are using it
Field:   id
Type:    ConstraintViolation

首先使用part_6的机器人有31 和 36,当删除part_6时,这个操作被阻止了,因为它被引用。下面看看产生的SQL:

[Wed, 03 Sep 14 14:15:40 +0800][INFO] SELECT `parts`.`id`, `parts`.`name` FROM `parts` WHERE `parts`.`id` = 7 LIMIT 1
[Wed, 03 Sep 14 14:15:40 +0800][INFO] SELECT `robots`.`id`, `robots`.`similar_robots_id`, `robots`.`name`, `robots`.`type`, `robots`.`year` FROM `robots` INNER JOIN `robots_parts` ON `robots_parts`.`robots_id` = `robots`.`id`  WHERE `robots_parts`.`parts_id` = :0
[Wed, 03 Sep 14 14:15:40 +0800][INFO] SELECT COUNT(*) AS `rowcount` FROM `robots_parts` WHERE `robots_parts`.`parts_id` = :0

先定位具体的part,然后通过part找出使用了它的robot(inner join操作),在删除part前到表robots_parts中检查对应的part是否被引用,如果被引用删除就被阻止了。

从这里可以看到,所谓的外键,是程序维护的,不是数据库意义上的外键,所有这里叫虚拟外键。所谓外键引用完整性就是这里的避免被引用的part被删除。

逻辑上,part属于robot一部分,part被引用时它不应该被删除,robot有多个part组成,robot删除时,它的引用信息也应该被删除(不是删除part哦)。
————————————————————————
Cascade/Restrict actions
Relationships that act as virtual foreign keys by default restrict the creation/update/deletion of records to maintain the integrity of data:

<?php

namespace Store\Models;

use Phalcon\Mvc\Model,
    Phalcon\Mvc\Model\Relation;

class Robots extends Model
{

    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany('id', 'Store\\Models\Parts', 'robots_id', array(
            'foreignKey' => array(
                'action' => Relation::ACTION_CASCADE
            )
        ));
    }

}

The above code set up to delete all the referenced records (parts) if the master record (robot) is deleted.
同样的,这个级联删除也是程序维护的。实际上,外键约束 和 级联删除这些,现代数据库系统都支持这些特征,可以把这些交给数据库系统来做,如果程序来维护开销是很大的,比如插入一条记录,要验证引用ID的有效性,需要针对被引用表来发起一次查询。

————————————————————————
注意到以上代码通过part找到使用了这个part的robots,实际过程产生了一条inner join查询,如果Parts只设置了hasMany()和RobotsParts产生对应,但是它是如果直接可以访问robots($part->robots)的呢?实际是不可以的,需要添加它跟robots的多对多关系才可以:

        $this->hasManyToMany(
            "id",
            "RobotsParts",
            "parts_id", "robots_id",
            "Robots",
            "id"
        );

这个就高速模型管理器,Parts和Robots的多对多关系通过RobotsParts实现,同理,要在Robot中获取Part,也需要在Robots中定义多对多关系,它们是产生JOIN的依据。

另外,我们在控制器代码中通过$part->robots获取使用这个part的robots,这个逻辑应该写入到自己的模型中:

    public function getRobots ($parameters=null)
    {
        return $this->getRelated('Robots', $parameters);
    }

这里的内容,就是Phalcon提供的ORM方案的核心部分。实际上,数据库查询操作还有更加复杂的内容,这些大概就是ORM无法封装的了,这个时候就要使用原生SQL了。

最后,这里提到了一对一的关系,但是没有具体内容。查看hasOne()方法有如下例子:

<?php

class Robots extends \Phalcon\Mvc\Model
{

   public function initialize()
   {
       $this->hasOne('id', 'RobotsDescription', 'robots_id');
   }

}

这里的意思就是Robots有一个对应的RobotsDescription。正如文档开头描述那样,hasOne应该是一个双向关系,hasManyToMany也是双向关系,而hasMany和belongsTo是单向的,所以在RobotsDescription中也应该定义hasOne。

Magento数据库适配器的获取分析

数据库适配器的管理是在Mage_Core_Model_Resource类中实现的。这个类没有继承任何类。类注释有一句:Resources and connections registry and factory,说是资源和连接注册表和工厂。

一般这个类只有一份实例,它注册到全局的注册表中:

Mage::getSingleton('core/resource'); //core/resource是硬编码

getSingleton方法不是在类级别上实现单例模式,是在全局中注册一个这样的类的时候,每次首先检查全局注册表,如果存储就不会建立。这个方法内部使用Mage::getModel()来找到模型(global/models/core -> Mage_Core_Model,然后组成Mage_Core_Model_Resource) Mage_Core_Model_Resource,返回一个实例。

我们一路跟进去,看它如何获取数据库适配器的(从getConnection()方法开始):

//一般$name = core_read 或 core_read等,每个资源都可以使用自定义的读写适配器,没有指定就使用默认的
//Mage_Core_Model_Resource-----
    public function getConnection($name)   
    {
        if (isset($this->_connections[$name])) {
            $connection = $this->_connections[$name];
            if (isset($this->_skippedConnections[$name]) && !Mage::app()->getIsCacheLocked()) {
                $connection->setCacheAdapter(Mage::app()->getCache());
                unset($this->_skippedConnections[$name]);
            }
            return $connection;
        }
        $connConfig = Mage::getConfig()->getResourceConnectionConfig($name); //global->resources->default_setup->connection
		//所有没有配置的都是用默认适配器 $connConfig在很情况下是false,所以_getDefaultConnection就让它使用默认的
        if (!$connConfig) {
            $this->_connections[$name] = $this->_getDefaultConnection($name);
            return $this->_connections[$name];
        }
        if (!$connConfig->is('active', 1)) {
            return false;
        }

        $origName = $connConfig->getParent()->getName();  //global->resources->default_setup
        if (isset($this->_connections[$origName])) {      //default_setup
            $this->_connections[$name] = $this->_connections[$origName];
            return $this->_connections[$origName];
        }

        $connection = $this->_newConnection((string)$connConfig->type, $connConfig);/**** 被执行 type->pdo_mysql 获取一个Varien_Db_Adapter_Pdo_Mysql适配器

        if ($connection) {
            if (Mage::app()->getIsCacheLocked()) {
                $this->_skippedConnections[$name] = true;
            } else {
                $connection->setCacheAdapter(Mage::app()->getCache());
            }
        }

        $this->_connections[$name] = $connection;
        if ($origName !== $name) {   //default_setup != core_read
            $this->_connections[$origName] = $connection;
        }
	// $this->_connections[default_setup] = $this->_connections[core_read] = $connection

        return $connection;
    }

//Mage_Core_Model_Config  ----->
    public function getResourceConnectionConfig($name)   //$name = core_read
    {
        $config = $this->getResourceConfig($name);
        if ($config) {
            $conn = $config->connection;
            if ($conn) {
                if (!empty($conn->use)) {  //global->resources->core_read/connection/use值为default_read
                    return $this->getResourceConnectionConfig((string)$conn->use);
                } else {
                    return $conn;
                }
            }
        }
        return false;
    }// 最终返回global->resources->default_setup->connection

    public function getResourceConfig($name)   //返回global->resources->core_read
    {
        return $this->_xml->global->resources->{$name};
    }

//Mage_Core_Model_Resource-----
    protected function _newConnection($type, $config)	//$type=pdo_mysql	$config = global->resources->default_setup->connection

    {
        if ($config instanceof Mage_Core_Model_Config_Element) {
            $config = $config->asArray();
        }
        if (!is_array($config)) {
            return false;
        }

        $connection = false;
        // try to get adapter and create connection
        $className  = $this->_getConnectionAdapterClassName($type); //$type=pdo_mysql  获得Varien_Db_Adapter_Pdo_Mysql
        if ($className) {
            // define profiler settings
            $config['profiler'] = isset($config['profiler']) && $config['profiler'] != 'false';

            $connection = new $className($config);   //使用global->resources->default_setup->connection 实例化Varien_Db_Adapter_Pdo_Mysql继承自Zend_Db_Adapter_Pdo_Mysql
            if ($connection instanceof Varien_Db_Adapter_Interface) {
                // run after initialization statements
                if (!empty($config['initStatements'])) {
                    $connection->query($config['initStatements']); //开始第一个查询
                }
            } else {
                $connection = false;
            }
        }

        // try to get connection from type
        if (!$connection) {
            $typeInstance = $this->getConnectionTypeInstance($type);
            $connection = $typeInstance->getConnection($config);
            if (!$connection instanceof Varien_Db_Adapter_Interface) {
                $connection = false;
            }
        }

        return $connection;
}

通过查找XML,最终适配器保存到了$this->_connections[default_setup] = $this->_connections[core_read] = $connection 中。

在每个资源类中,都可以通过getReadConnection()方法(内部调用_getReadAdapter)来获取这个适配器对象。 当要和数据库交换时,可以使用资源类提供的包装方法(内部会调用适配器来完成),比如load() save() delete()等,也可以直接使用适配器提供的方法。

注意看getConnection()方法开始的代码,说明连接对象是会被缓存的,这样做是有好处的,具体是如何实现的就属于缓存方案的问题了。

还必须一提的是:Mage::getConfig()->getResourceConnectionConfig($name)

    public function getResourceConnectionConfig($name)
    {
        $config = $this->getResourceConfig($name);
        if ($config) {
            $conn = $config->connection;
            if ($conn) {
                if (!empty($conn->use)) {
                    return $this->getResourceConnectionConfig((string)$conn->use);
                } else {
                    return $conn;
                }
            }
        }
        return false;
}

    public function getResourceConfig($name)
    {
        return $this->_xml->global->resources->{$name};
    }

比如我传递了core_read进去,getResourceConfig返回 global/resources/core_read(如果找不到就返回false),那么getResourceConnectionConfig中就进入connection节点,然后判读这个节点是否有use,如果有再重复这个过程。然后返回global/resources/core_read/connection,这个就是作为创建适配器的参数。如果返回false就是用默认适配器,在Magento早期版本,资源模型还必须配置读写适配器,后来版本修改了这个过程,如果没有定义就使用默认的。 这个过程可以推导出如果要自定义适配器,需要在模块的config.xml文件中

<global>
	<resources>
	<模块名_read>
		<connection>
<host>localhost</host>
<username>root</username>
<password>root</password>
<dbname>magento</dbname>
<initStatements>SET NAMES utf8</initStatements>
<model>mysql4</model>
<type>pdo_mysql</type>
<pdoType/>
<active>1</active>
</connection>
	</模块名_read>
</resources>
</global>

在创建连接时最后还有这段代码

        if (!$connection) {
            $typeInstance = $this->getConnectionTypeInstance($type);
            $connection = $typeInstance->getConnection($config);
            if (!$connection instanceof Varien_Db_Adapter_Interface) {
                $connection = false;
            }
        }

如果适配器创建失败了,那么就通过类型视图获取实例,比如这里传递pdo_mysql,那么先去配置文件找global/ resource/ connection/types/类型名(这里是pdo_mysql),取出类名Mage_Core_Model_Resource_Type_Db_Pdo_Mysql,然后就直接new这个类,通过查看源代码,最终还是会实例化和type相关的适配器类,比如这里是Varien_Db_Adapter_Pdo_Mysql类。总体上看,比较粗糙。

另外,在资源模型中,最好总是使用$this->_getReadAdapter() 和 $this->_getWriteAdapter(),这两个方法命名不但非常直观,还主要在于_getReadAdapter()方法内部会判断是否启动了事务,如果启动了就返回写适配器作为读适配器。另外,$this->getReadConnection()方法是$this->_getReadAdapter()包装器。

Magento数据库抽象

可以看到Magento的数据库抽象使用了Zend Framework的Pdo_MySql抽象,不过它实现了自己的Varien_Db_Adapter_Interface接口。 Statement和Select都构架在Zend Framework的原有实现上。

从实现原理上看,过程并不复杂,但是涉及的方法非常多。肯定是无法全部记得住的。不过可以研究系统模块获取最常使用的范例代码,这样可以加速学习进度。

首先,每一个模块都可以有几部分组成,很关键的一个就是模型,每个模型可以对应有资源模型,资源模型可以操作数据库,但是能操作那些数据表(实体)是必须写入配置文件的,因为获取实体对应的表名需要搜索全局XML(也是可以不写,只要跳过到配置文件检查即可,比如直接写数据表名,而不是通过资源模型本身提供的getTable getMainTable来先从全局XML获取实体,然后才获得表名,但是这个方法会自动验证表是否存在)。比如Core模块的实体配置有(注意,分配实体一定是在资源模型中,因为只有它才需要):

           <core_resource>
                <class>Mage_Core_Model_Resource</class>
                <deprecatedNode>core_mysql4</deprecatedNode>
                <entities>
                    <config_data>
                        
                    <table>core_config_data</table></config_data>
                    <website>
                        
                    <table>core_website</table></website>
                    <store>
                        
                    <table>core_store</table></store>
                    <resource>
                        <table>core_resource</table>
                    </resource>
                    <cache>
                        <table>core_cache</table>
                    </cache>
                    <cache_tag>
                        <table>core_cache_tag</table>
                    </cache_tag>
                    <cache_option>
                        <table>core_cache_option</table>
                    </cache_option>
		</entities>
            </core_resource>

以上并没有把全部的实体都列出来。下面看一些例子(来自Mage_Core_Model_Resource_Config类的loadToXml方法)

    public function loadToXml(Mage_Core_Model_Config $xmlConfig, $condition = null)
    {
        $read = $this->_getReadAdapter();
        if (!$read) {
            return $this;
        }
        $websites = array();
        $select = $read->select()
            ->from($this->getTable('core/website'), array('website_id', 'code', 'name'));
        $rowset = $read->fetchAssoc($select);
        foreach ($rowset as $w) {
            $xmlConfig->setNode('websites/'.$w['code'].'/system/website/id', $w['website_id']);
            $xmlConfig->setNode('websites/'.$w['code'].'/system/website/name', $w['name']);
            $websites[$w['website_id']] = array('code' => $w['code']);
        }
	//………
}

这是一个很简洁的例子,使用_getReadAdapter获取适配器,符合刚才的分析。然后直接调用适配器的select()方法获取一个Varien_Db_Select对象,然后用它来构建查询,这里调用了它的from方法,第一参数指定表明,第二参数用数组指定了要查询的列。然后把这个对象直接传递给适配器的fetchAssoc方法,它就返回了一个关联数组,它包保存了从数据库返回的数据。

至于Varien_Db_Select的使用,由于它继承自Zend_Db_Select,使用上自然会保存一致(http://blog.ifeeline.com/429.html)。Varien_Db_Select实现了构建Left Join联结这样的功能,填补了Zend_Db_Select的不足。

你可以看到,大部分的SQL的操作,都是首先使用Select构建查询,然后再传递给适配器对象的。对应EAV模型,如果Select不支持构造左联结,那你就只能写SQL了。

Mage_Core_Model_Resource类实例作为一个单例存在,意味着我随时可以获取它的引用(Mage::getSingleton(‘core/resource’)),比如在我自己的控制器方法中,然后调用它的getConnection()来获取适配的引用(如果是想获取读适配就传递read,如果想获取写适配器就传递write,留空就默认返回写适配器)。适配器获取后,你可以任意操作数据库(这个是绕开Magento MVC的规定:使用模型和资源模型的方法)。

永久连接: http://blog.ifeeline.com/455.html
原创文章,转载务必保留出处。