标签归档:db

Laravel 数据库封装

入门

数据库相关配置文件都在 config/database.php。主要看MySQL的配置:

    'connections' => [
        'mysql' => [
            'driver'    => 'mysql',
            'host'      => env('DB_HOST', 'localhost'),
            'database'  => env('DB_DATABASE', 'forge'),
            'username'  => env('DB_USERNAME', 'forge'),
            'password'  => env('DB_PASSWORD', ''),
            'charset'   => 'utf8',
            'collation' => 'utf8_unicode_ci',
            'prefix'    => '',
            'strict'    => false,
        ]
    ]

注意这里使用env()方法,它从环境变量中读值。第一参数就是key,第二参数是默认值。说的直接点,env()就是从跟目录下的.env文件中(这些值会读入作为环境变量)读取相关的配置值。

以下是一个读写分离的例子:

'mysql' => [
    'read' => [
        'host' => '192.168.1.1',
    ],
    'write' => [
        'host' => '196.168.1.2'
    ],
    'driver'    => 'mysql',
    'database'  => 'database',
    'username'  => 'root',
    'password'  => '',
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix'    => '',
],

注意看,read和write同级的配置,就可以放入其中,用来定制读写分离链接。

Laravel提供了一个叫DB的Facade,Facade实现的实际就是以静态方法调用对应实例的的方法,就像是实例方法的修饰。以下是一份对照表:

App     app     Illuminate\Container\Container
Atisan      Illuminate\Contracts\Console\Kernel 接口,index.php中会写入绑定,命令行程序时会实例化
Auth        auth        Illuminate\Auth\AuthManager
Blade       static::$app['view']->getEngineResolver()->resolve('blade')->getCompiler();
Bus'        Illuminate\Contracts\Bus\Dispatcher' 接口,在实际调用时实例化
Cache       cache       Illuminate\Cache\CacheManager   
Config      config  Illuminate\Config\Repository
Cookie      cookie      Illuminate\Cookie\CookieJar
Crypt       encrypter       Illuminate\Encryption\Encrypter
DB          db      Illuminate\Database\DatabaseManager
Eloquent    => Illuminate\Database\Eloquent\Model::class ***
Event       events      Illuminate\Events\Dispatcher
File        files       Illuminate\Filesystem\Filesystem
Gate        Illuminate\Contracts\Auth\Access\Gate
Hash        hash
Input       request Illuminate\Http\Request"
Inspiring   => Illuminate\Foundation\Inspiring::class,
Lang        translator  Illuminate\Translation\Translator
Log         log     Illuminate\Log\Writer
Mail        mailer      Illuminate\Mail\Mailer
Password    auth.password   Illuminate\Auth\Passwords\PasswordBroker
Queue       queue       Illuminate\Queue\QueueManager"
Redirect    redirect    Illuminate\Routing\Redirector
Redis'      redis       Illuminate\Redis\Database
Request     request     Illuminate\Http\Request
Response    Illuminate\Contracts\Routing\ResponseFactory 对应这样接口的实例
Route       router      Illuminate\Routing\Router
Schema      static::$app['db']->connection()->getSchemaBuilder();
Session     session     Illuminate\Session\SessionManager
Storage     filesystem  Illuminate\Filesystem\FilesystemManager
URL     url     Illuminate\Routing\UrlGeneratoralidator     
validator   Illuminate\Validation\Factory
View        view        Illuminate\View\Factory

我们可以自定义Facade,然后写入到conf/app.php中的aliases中即可。这是一个可以充分使用的工具,因为对应的实例可以通过容器自动判断而生成。

执行查询

$results = DB::select('select * from users where id = ?', [1]);

如果去Illuminate\Database\DatabaseManager看,这个类实现Illuminate\Database\ConnectionResolverInterface接口,作为一个Manager,自然需要有默认的链接,获取链接之类的方法。另外,同样作为一个Manager,它要能管理不同的链接,所以它依赖一个叫Illuminate\Database\Connectors\ConnectionFactory的工厂,当要产生具体的数据库链接时,就要用到这个工厂生产。它生成的对象都是继承自Illuminate\Database\Connection类的,具体来说Illuminate\Database\MySqlConnection继承自Illuminate\Database\Connection,它自然就是所谓适配器了,所有的链接,都有相同的接口(我认为这个实现做到了简单,而且不失灵活性)。记住,你要的,大部分都在Illuminate\Database\Connection中。

在Illuminate\Database\DatabaseManager中:

public function __call($method, $parameters)
    {
        return call_user_func_array([$this->connection(), $method], $parameters);
    }

对于不存在的方法,它去调用具体的链接对象的方法,这就去到了Illuminate\Database\Connection类中。可以看看select()方法的实现,内部操作PDO….(这个已经不是我要关注的重点了)

// 返回数组
$results = DB::select('select * from users where id = ?', [1]);
// PDO语法
$results = DB::select('select * from users where id = :id', ['id' => 1]);

// 插入
DB::insert('insert into users (id, name) values (?, ?)', [1, 'Dayle']);
// 更新
DB::update('update users set votes = 100 where name = ?', ['John']);
// 删除
DB::delete('delete from users');

// 执行statement
DB::statement('drop table users');

// 可以使用 DB::listen 方法,去监听查找的事件
// 玛尼
DB::listen(function($sql, $bindings, $time)
{
    //
});

// 事务处理,这个搞法确实很优雅
// 在 transaction 闭包若抛出任何异常会导致事务自动回滚
// 那么如果条件不符合,抛一个异常?
DB::transaction(function()
{
    DB::table('users')->update(['votes' => 1]);

    DB::table('posts')->delete();
});

// 手动事务,很常见
DB::beginTransaction();
DB::rollback();
DB::commit();

// foo是数据库名称
$users = DB::connection('foo')->select(...);
// 取用原始底层的 PDO 实例
$pdo = DB::connection()->getPdo();
// 重新连接到特定的数据库
DB::reconnect('foo');
// 超过了底层 PDO 实例的 max_connections 的限制,需要关闭特定的数据库连接
DB::disconnect('foo');

// 启用查询日志 调试时比较有用
DB::connection()->enableQueryLog();
// 获取查询日志
$queries = DB::getQueryLog();

查询构造器
这个货视乎总是少不了的。看过很多实现,Zend\Db的查询构造器最操蛋。看看Laravel如何给我们优雅的实现吧。

// 从数据表中取得所有的数据行
$users = DB::table('users')->get();

foreach ($users as $user)
{
    var_dump($user->name);
}

// 从数据表中分块查找数据列,这个如果无法通过SQL实现时,需要遍历查找时,是一个非常不错的方法
DB::table('users')->chunk(100, function($users)
{
    foreach ($users as $user)
    {
        //
    }
});

// 返回false停止查找
DB::table('users')->chunk(100, function($users)
{
    //

    return false;
});

// 取一行数据
$user = DB::table('users')->where('name', 'John')->first();
// pluck摘的意思,它跟$user->name是一样的,都是从第一行中摘
$name = DB::table('users')->where('name', 'John')->pluck('name');

// 获取字段的一列的值,有时是很便利的
$roles = DB::table('roles')->lists('title');
// 这个搞法返回一个关联数据,第二参数为数组索引,不得不说,这个有时候真的非常有用
$roles = DB::table('roles')->lists('title','id');

// select是表示选择字段之意
$users = DB::table('users')->select('name', 'email')->get();
$users = DB::table('users')->distinct()->get();
$users = DB::table('users')->select('name as user_name')->get();
// 增加字段
$query = DB::table('users')->select('name');
$users = $query->addSelect('age')->get();

// where语法
$users = DB::table('users')->where('votes', '>', 100)->get();
$users = DB::table('users')
                    ->where('votes', '>', 100) 
                    ->orWhere('name', 'John') // 默认是等于
                    ->get();

$users = DB::table('users')
                    ->whereBetween('votes', [1, 100])->get();
$users = DB::table('users')
                    ->whereNotBetween('votes', [1, 100])->get();

$users = DB::table('users')
                    ->whereIn('id', [1, 2, 3])->get();
$users = DB::table('users')
                    ->whereNotIn('id', [1, 2, 3])->get();

$users = DB::table('users')
                    ->whereNull('updated_at')->get();


// 这个搞法有点疯狂,建议不要使用
$admin = DB::table('users')->whereId(1)->first();
$john = DB::table('users')
                    ->whereIdAndEmail(2, 'john@doe.com')
                    ->first();
$jane = DB::table('users')
                    ->whereNameOrAge('Jane', 22)
                    ->first();

// 排序分组
$users = DB::table('users')
                    ->orderBy('name', 'desc')
                    ->groupBy('count')
                    ->having('count', '>', 100)
                    ->get();

// Limit语法
$users = DB::table('users')->skip(10)->take(5)->get();

// JOIN
DB::table('users')
            ->join('contacts', 'users.id', '=', 'contacts.user_id')
            ->join('orders', 'users.id', '=', 'orders.user_id')
            ->select('users.id', 'contacts.phone', 'orders.price')
            ->get();

DB::table('users')
        ->leftJoin('posts', 'users.id', '=', 'posts.user_id')
        ->get();

DB::table('users')
        ->join('contacts', function($join)
        {
            $join->on('users.id', '=', 'contacts.user_id')->orOn(...);
        })
        ->get();

DB::table('users')
        ->join('contacts', function($join)
        {
            $join->on('users.id', '=', 'contacts.user_id')
                 ->where('contacts.user_id', '>', 5);
        })
        ->get();
// 如果有更加复杂的JOIN,个人还是情愿去写SQL

// 高级Where,这个方式个人觉得很满意
// 可以套几套写成复杂where过滤
DB::table('users')
            ->where('name', '=', 'John')
            ->orWhere(function($query)
            {
                $query->where('votes', '>', 100)
                      ->where('title', '<>', 'Admin');
            })
            ->get();
//select * from users where name = 'John' or (votes > 100 and title <> 'Admin')

// 
DB::table('users')
            ->whereExists(function($query)
            {
                $query->select(DB::raw(1))
                      ->from('orders')
                      ->whereRaw('orders.user_id = users.id');
            })
            ->get();
select * from users
where exists (
    select 1 from orders where orders.user_id = users.id
)

// 聚合 这个不满意,看来要写SQL(说明再牛逼的封装,也离不开SQL)
$users = DB::table('users')->count();
$price = DB::table('orders')->max('price');
$price = DB::table('orders')->min('price');
$price = DB::table('orders')->avg('price');
$total = DB::table('users')->sum('votes');

// 使用原生表达式(看例子就好)
$users = DB::table('users')
                     ->select(DB::raw('count(*) as user_count, status'))
                     ->where('status', '<>', 1)
                     ->groupBy('status')
                     ->get();

DB::table('users')->insert(
    ['email' => 'john@example.com', 'votes' => 0]
);

// 注意,如果要返回插入的ID,你需要这么干
$id = DB::table('users')->insertGetId(
    ['email' => 'john@example.com', 'votes' => 0]
);

// 批量插入(非常让人满意)
DB::table('users')->insert([
    ['email' => 'taylor@example.com', 'votes' => 0],
    ['email' => 'dayle@example.com', 'votes' => 0]
]);

// 更新 where的使用方法跟查询是一样的
DB::table('users')
            ->where('id', 1)
            ->update(['votes' => 1]);

// 自增自减一个字段
DB::table('users')->increment('votes');
DB::table('users')->increment('votes', 5);
DB::table('users')->decrement('votes');
DB::table('users')->decrement('votes', 5);

//?
DB::table('users')->increment('votes', 1, ['name' => 'John']);

// 删除 where的使用方法跟查询是一样的
DB::table('users')->where('votes', '<', 100)->delete();

//
DB::table('users')->delete();
DB::table('users')->truncate();

// 合并查询
$first = DB::table('users')->whereNull('first_name');
$users = DB::table('users')->whereNull('last_name')->union($first)->get();

// 上锁 取值更新可能需要上锁,这个也是很满意的实现
DB::table('users')->where('votes', '>', 100)->sharedLock()->get();
DB::table('users')->where('votes', '>', 100)->lockForUpdate()->get();

Laravel的DB封装确实做到了优雅,没有过度设计,但是却提供了非常灵活的实现。

Zend\Db TableGateway操作

Zend\Db TableGateway操作个人觉得设计不够友好。在内部如果需要构建复杂查询,还是直接SQL来得实在。但是对于简单的CURL,它就非常的擅长。Zend\Db 表的封装比1.x时代还少,大部分同学真正要的是简便的CURD操作,复杂的SQL,还是需要自己组装。不过Select构建器,还是基本可以满足很多情况的。

$table = new Zend\Db\TableGateway\TableGateway('product',$adapter);


// 从TableGateway返回Sql,再从Sql返回一个Select对象
// 这个Select表名是已知的
// 相对于执行了$select->from($table)
$select = $table->getSql()->select();

/*
$select->where('id > 10')->order('id DESC')->limit(10);

// 然后把这个Select对象丢给TableGateway的selectWith()方法
// selectWith()只接收Select对象,select()方法接收Where对象等
$resultSet = $table->selectWith($select);
print_r($resultSet->toArray());
*/

/*
$select->where(function($where){
    $where->lessThan('id',10);
    $where->greaterThan('id',5);
    return $where;
})->order('id DESC')->limit(10);

$resultSet = $table->selectWith($select);
print_r($resultSet->toArray());
*/

/*
// 获取Select的Where对象,不能用where()方法,因为它是构建where条件
// 所以比较操蛋,绝对的不舒服
$where = $select->where;
$where->lessThan('id',10);
$where->greaterThan('id',5);
$select->order('id DESC')->limit(10);

$resultSet = $table->selectWith($select);
print_r($resultSet->toArray());
*/

// where可以是一个字符串,也可以是一个数组,字符串表示条件一个条件
// 第二参数表示是OR 或  AND,默认为AND,但是第一个where不会添加前置符
// 链式操作可以添加多个条件,是OR 是  AND由后面的where的第二参数决定 
// $sqlSring = $select->where('id > 30','OR')->where('id < 40','OR')->getSqlString();
// SELECT "product".* FROM "product" WHERE id > 30 OR id < 40

// where()方法还可接收一个数组,数组的单元是每个条件,这些条件是OR是AND还是由第二参数决定
// 可以看到,这样搞法很容易引起歧义
// $sqlSring = $select->where(array('id > 30','name like "%vfeelit%"'))->where(array('id > 40'),'OR')->getSqlString();
// SELECT "product".* FROM "product" WHERE id > 30 AND name like "%vfeelit%" OR id > 40

$select->where(function($where){
    $subWhereId = clone $where;
    $subWhereTitle = clone $where;
    
    $subWhereId->lessThan('id',10);
    //$subWhereId->or;
    $subWhereId->greaterThan('id',20);
    $where->addPredicate($subWhereId);
    
    $subWhereTitle->equalTo('title', 'a');
    //$subWhereTitle->or;
    $subWhereTitle->equalTo('title', 'b');
    $where->addPredicate($subWhereTitle);
    
    return $where;
});
// 这就是构建复杂查询的方法,有没有活生生把人气死的感觉?
// addPredicate就是添加一个断言,就是一个整体,用括号括起来
$sqlSring = $select->getSqlString();

注:翻回来看看,Zend Framework这个实现也有点落伍了。3.x不兼容2.x是分分钟有可能的事情。这个官方出品的东西,视乎总无法把握得住发展主流(MVC部分)。不过,它的一些组件个人还是觉得不错的,我也经常使用。DB封装这块,看起来到3.x时,估计也要重写了。

Zend Framework 2.x 之 Zend\Db

zend-db

Driver是数据库驱动抽象,负责和具体的数据库建立链接,如果要获取这个链接,就需要通过Adapter的Driver。Platform是针对不同平台的抽象,比如不同平台用不同的字符把字段圈起来。

Adapter需要确定Driver 和 Platform,对不同驱动和平台,Adapter提供了一个通用接口,这个就是适配器设计模式了。

$adapter = new Zend\Db\Adapter\Adapter(array(
		'driver' => 'pdo',
		'dsn' => 'mysql:dbname=test;hostname=localhost',
		'username' => 'root',
		'password' => '',
		'driver_options' => array(
				PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
		),
));

$adapter = new Zend\Db\Adapter\Adapter(array(
    'hostname' => 'localhost',
    'database' => 'test',
    'username' => 'root',
    'password' => '',
    
    'driver' => 'mysqli',
    'charset' =>'utf8'
    
    //'driver' => 'pdo_mysql',
    //'driver_options' => array(
    //    PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
    //),
));
//当不使用PDO时,字符集的指定使用charset,有时候我们有理由相信mysqli驱动会比PDO好,因为它抽象多了一层。注意:不管是否使用PDO,driver_options都可以使用,不过需要指定针对具体驱动的选项。

这是一段范本代码。看看手册中的说法:
“It is important to know that by using this style of adapter creation, the Adapter will attempt to create any dependencies that were not explicitly provided. A Driver object will be created from the configuration array provided in the constructor. A Platform object will be created based off the type of Driver class that was instantiated. And lastly, a default ResultSet object is created and utilized. Any of these objects can be injected, to do this, see the next section.”

如果使用这种风格创建适配器,适配器试图去创建所有的没有明确提供的依赖(依赖具体的驱动,平台)。一个Driver对象将根据适配器构造函数中提供的数组参数来创建。一个Platform对象将根据Driver被实例化时的类的类型来创建。最后,一个默认的ResultSet对象被创建并可用。所有这些对象都是可注入的。

use Zend\Db\Adapter\Platform\PlatformInterface;
use Zend\Db\ResultSet\ResultSet;

class Zend\Db\Adapter\Adapter {
    public function __construct($driver, PlatformInterface $platform = null, ResultSet $queryResultSetPrototype = null)
}

玛尼,意思就是你可以通过构造函数去指定具体的驱动,平台,结果集。(第一参数可以是配置数组 或 Driver对象)

适配器提供一个相对原始的query()方法,可以预处理SQL语句:

$result = $adapter->query('SELECT * FROM `site` WHERE `id` = ?', array(5));

内部工作流程:
1 创建一个新的Statement对象
2 如果需要,预处理一个数组到ParameterContainer
3 把ParameterContainer注入到Statement对象
4 执行Statement对象,处理结果对象
5 检查结果对象确定提供的sql是查询或or a result set producing statement
6 如果是结果的语句,克隆ResultSet的prototype,inject Result as datasource
7 否则返回Result

也可直接执行SQL语句,参考:

$r = $adapter->query('ALTER TABLE `site` ADD `test` int NULL', Zend\Db\Adapter\Adapter::QUERY_MODE_EXECUTE);

一般执行最多的是SELECT INSERT UPDATE DELETE语句,以下是一个范本:

$sttm = $adapter->query('SELECT * FROM `site`');
$resultSet = $sttm ->execute();
foreach($resultSet as $rr){
	print_r($rr);
}

最好可以去看看query()方法的实现:

    public function query($sql, $parametersOrQueryMode = self::QUERY_MODE_PREPARE)
    {
        if (is_string($parametersOrQueryMode) && in_array($parametersOrQueryMode, array(self::QUERY_MODE_PREPARE, self::QUERY_MODE_EXECUTE))) {
            $mode = $parametersOrQueryMode;
            $parameters = null;
        } elseif (is_array($parametersOrQueryMode)) {
            $mode = self::QUERY_MODE_PREPARE;
            $parameters = $parametersOrQueryMode;
        } else {
            throw new Exception\InvalidArgumentException('Parameter 2 to this method must be a flag, an array, or ParameterContainer');
        }

        if ($mode == self::QUERY_MODE_PREPARE) {
            $this->lastPreparedStatement = null;
            $this->lastPreparedStatement = $this->driver->createStatement($sql);
            $this->lastPreparedStatement->prepare();
            if (is_array($parameters) || $parameters instanceof ParameterContainer) {
                $this->lastPreparedStatement->setParameterContainer((is_array($parameters)) ? new ParameterContainer($parameters) : $parameters);
                $result = $this->lastPreparedStatement->execute();
            } else {
                return $this->lastPreparedStatement;
            }
        } else {
            $result = $this->driver->getConnection()->execute($sql);
        }

        if ($result instanceof Driver\ResultInterface && $result->isQueryResult()) {
            $resultSet = clone $this->queryResultSetPrototype;
            $resultSet->initialize($result);
            return $resultSet;
        }

        return $result;
    }

第二参数默认是预处理(self::QUERY_MODE_PREPARE)。首先如果是字符串(预处理 或 直接执行),参数就是null,如果是一个数组,那么就是预处理,参数就是这个数组。

如果是预处理,就调用驱动的createStatement($this->driver->createStatement($sql)),然后执行statement的prepare方法,如果有参数提供,就调用setParameterContainer方法,然后执行statement的execute方法,如果没有参数提供就直接把这个statement返回。

如果不是预处理,直接调用:$result = $this->driver->getConnection()->execute($sql);

最后判断是否是查询,是查询就返回结果集。否则就是一个结果对象。

总结,三种可能,statement对象,结果对象,结果集对象。首先根据是否是预处理,如果是但是没有绑定参数,直接返回statement对象,如果需要绑定参数,则把执行结果用结果对象保存起来,如果是执行语句,也把结果用结果对象保存,最后根据sql是否是查询,确定是返回结果集还是结果对象。

事实:如果返回结果集,必定是查询。对于查询,这里只是再次封装一遍而已。 实际不用再次封装也是可以使用的。以下是一些例子,对比返回的类型,可以更好理解:

// 不指定第二参数,为预处理模式,没有需要的参数绑定,直接返回Statement
// Zend\Db\Adapter\Driver\Pdo\Statement
$stmt = $adapter->query('SELECT * FROM `site`');
// Zend\Db\Adapter\Driver\Pdo\Result
$result= $stmt->execute();
foreach($result as $rr){
	//print_r($rr);
}

// 第二参数为一个数组,为预处理模式,绑定的参数为指定的数组,内部执行Statement的execute
// 返回结果或结果集
// Zend\Db\ResultSet\ResultSet
$resultSet = $adapter->query('SELECT * FROM `site` WHERE `id` > ?', array(1));
foreach($resultSet as $rr){
	//print_r($rr);
}

// 第二参数直接指定为执行模式,返回结果或结果集
// 可以看做和上面用法等同
// Zend\Db\ResultSet\ResultSet
$resultSet = $adapter->query('SELECT * FROM `site`', Zend\Db\Adapter\Adapter::QUERY_MODE_EXECUTE);
foreach($resultSet as $rr){
	//print_r($rr);
}

// 直接执行模式,返回结果(一般INSERT UPDATE这些返回一个结果,查询返回结果集)
// Zend\Db\Adapter\Driver\Pdo\Result
$result = $adapter->query("INSERT INTO `site`(`platform_id`,`name`) values('2','中国')", Zend\Db\Adapter\Adapter::QUERY_MODE_EXECUTE);
// 返回插入的ID
echo $result->getGeneratedValue();


// Zend\Db\Adapter\Driver\Pdo\Result
$result = $adapter->query("UPDATE `site` SET `name` = '中国' where id=12", Zend\Db\Adapter\Adapter::QUERY_MODE_EXECUTE);
// 更新受影响的行数
echo $result->getAffectedRows();

这里需要说明的是,getAffectedRows()是Zend\Db\Adapter\Driver\Pdo\Result的方法,返回的是受影响的行数,而Zend\Db\ResultSet\ResultSet并没有这个方法,Zend\Db\ResultSet\ResultSet的toArray()可以把结果集转换成一个数组输出。

Zend\Db\ResultSet\ResultSet是一个更抽象的东西,封装了Zend\Db\ResultSet\Result。

在理解了以上内容的前提下,我们可以不使用query(),而直接使用statement。

// Zend\Db\ResultSet\Result
$statement = $adapter->createStatement($sql, $optionalParameters);
$result = $statement->execute();

注意看上面的例子中的$adapter->createStatement(),这个方法内部实际调用$this->driver->createStatement(),driver中提供了三个对象:

Zend\Db\Adapter\Driver\ConnectionInterface
Zend\Db\Adapter\Driver\StatementInterface
Zend\Db\Adapter\Driver\ResultInterface

Statement 和 Result上面已经讨论过了(通过adapter中间接调用,adapter中的query()会调用到driver的execute)。以下是driver中必须知道的方法:

$cnn = $adapter->getDriver()->getConnection();
$cnn->beginTransaction(); 	//开始事务
$cnn->commit();			//提交事务
$cnn->rollback();		//回滚事务
$cnn->isConnected()		//是否已链接
$cnn->getLastGeneratedValue()   //最后插入的ID

适配器在自动构建SQL语句时,需要它的另一个对象支持,Zend\Db\Adapter\Platform。直接看例子:

$platform = $adapter->getPlatform();
// or
$platform = $adapter->platform; // magic property access


/** @var $adapter Zend\Db\Adapter\Adapter */
/** @var $platform Zend\Db\Adapter\Platform\Sql92 */
$platform = $adapter->getPlatform();

// "first_name"
echo $platform->quoteIdentifier('first_name');

// "
echo $platform->getQuoteIdentifierSymbol();

// "schema"."mytable"
echo $platform->quoteIdentifierChain(array('schema','mytable')));

// '
echo $platform->getQuoteValueSymbol();

// 'myvalue'
echo $platform->quoteValue('myvalue');

// 'value', 'Foo O\\'Bar'
echo $platform->quoteValueList(array('value',"Foo O'Bar")));

// .
echo $platform->getIdentifierSeparator();

// "foo" as "bar"
echo $platform->quoteIdentifierInFragment('foo as bar');

// additionally, with some safe words:
// ("foo"."bar" = "boo"."baz")
echo $platform->quoteIdentifierInFragment('(foo.bar = boo.baz)', array('(', ')', '='));

Zend\Db\Sql\Sql提供了比较强大的,面向对象的SQL书写方法。

use Zend\Db\Sql\Sql;
$sql = new Sql($adapter);
$select = $sql->select(); // @return Zend\Db\Sql\Select
$insert = $sql->insert(); // @return Zend\Db\Sql\Insert
$update = $sql->update(); // @return Zend\Db\Sql\Update
$delete = $sql->delete(); // @return Zend\Db\Sql\Delete

Sql对象接收$adapter,第二参数可以是表名。具体例子:

###返回一个Statement,然后调用它的execute方法返回结果集
use Zend\Db\Sql\Sql;
$sql = new Sql($adapter);
$select = $sql->select();
$select->from('foo');
$select->where(array('id' => 2));

$statement = $sql->prepareStatementForSqlObject($select);
$results = $statement->execute();

###换成查询字符串,传递到适配器的query()方法
use Zend\Db\Sql\Sql;
$sql = new Sql($adapter);
$select = $sql->select();
$select->from('foo');
$select->where(array('id' => 2));

$selectString = $sql->getSqlStringForSqlObject($select);
$results = $adapter->query($selectString, $adapter::QUERY_MODE_EXECUTE);


### 指定第二参数,不需要指定表。
use Zend\Db\Sql\Sql;
$sql = new Sql($adapter, 'foo');
$select = $sql->select();
$select->where(array('id' => 2)); // $select already has the from('foo') applied

这部分的内容,如果熟悉SQL,很容易就可以掌握,实际就是以面向对象的方法构建可以适配多平台的SQl。

可以使用以上例子那样使用Sql对象,不过大多配合TableGateway来使用。

可以直接实例化一个TableGateway对象,比如:

$tg = new Zend\Db\TableGateway\TableGateway(“test”,$adapter);

第一参数为表名,第二参数为适配器对象。

大部分情况下都不这样做,一般使用自己的类继承一下Zend\Db\TableGateway\TableGateway类,或者写一个公共类,然后所有类都从这个公共类继承。比如,我们可以把表和类对应起来,一一建立对应的TableGateway类,安装最佳实践,应该建立一个公共类,就算当前没有可以公共的方法(未来可能有),也是如此。每个表建立对应的TableGateway,我们就可以在类中添加复杂的业务逻辑(关注数据交互)。
tablegateway

构造函数在TableGateway中提供:

public function __construct($table,AdapterInterface $adapter){
	$this->table = $table;
	$this->adapter = $adapter;

	$this->sql = ($sql)?:new Sql($this->adapter,$this->table);

	$this->initialize();
}

可做如下封装:

// 常规封装
class MyTableGateway extends Zend\Db\TableGateway\TableGateway{
	protected $table = ‘my_table_gateway’;

	public function __construct($adapter){
		parent::__construct($this->table, $adapter);
	}
}

// 这个注入方式简单粗暴
class MyTableGateway extends Zend\Db\TableGateway\TableGateway{
	protected $table = ‘my_table_gateway’;

	public function __construct(){
		global $adapter;
		if($adapter instanceof Zend\Db\Adapter\Adapter){
			parent::__construct($this->table, $adapter);
		}else{
			//抛异常
		}
	}
}

// 或者使用
Registry::set(‘adapter’,$adapter);
class MyTableGateway extends Zend\Db\TableGateway\TableGateway{
	protected $table = ‘my_table_gateway’;

	public function __construct(){
		$adapter = Registry::get(‘adapter’);
		if($adapter instanceof Zend\Db\Adapter\Adapter){
			parent::__construct($this->table, $adapter);
		}else{
			//抛异常
		}
	}
}

// 更好的依赖注入
class Registry{
	public static $instance = array();
	
	public static function set($name,$callBack){
		if(!isset(self::$instance[$name])){
			self::$instance[$name] = $callBack;
		}
	}
	
	public static function get($name){
		if(isset(self::$instance[$name])){
			if(is_callable(self::$instance[$name])){
				$func = self::$instance[$name];
				self::$instance[$name] = $func();
			}
			return self::$instance[$name];
		}
		return false;
	}
}

Registry::set('adapter',function(){
	$adapter = new Zend\Db\Adapter\Adapter(array(
			'driver' => 'pdo',
			'dsn' => 'mysql:dbname=test;hostname=localhost',
			'username' => 'root',
			'password' => '',
			'driver_options' => array(
					PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
			),
	));
	return $adapter;
});

class MyTableGateway extends Zend\Db\TableGateway\TableGateway{
	protected $table = ‘my_table_gateway’;

	public function __construct(){
		$adapter = Registry::get(‘adapter’);
		if($adapter instanceof Zend\Db\Adapter\Adapter){
			parent::__construct($this->table, $adapter);
		}else{
			//抛异常
		}
	}
}

//最总改进范本
use Zend\Db\TableGateway\TableGateway;
use Zend\Db\Adapter\Adapter;
use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\Sql\Sql;

class MyTableGateway extends Zend\Db\TableGateway\TableGateway{
	protected $table = ‘my_table_gateway’;

		public function __construct(AdapterInterface $adapter = null, $features = null, ResultSetInterface $resultSetPrototype = null, Sql $sql = null){
			if($adapter instanceof Adapter){
				parent::__construct($this->table, $adapter, $features, $resultSetPrototype, $sql);
			}else{
				$adapter = Registry::get('adapter');
				if($adapter instanceof Adapter){
					parent::__construct($this->table, $adapter);
				}else{
					throw new Exception("Need an Zend\Db\Adapter object.");
				}
			}
}
}

最后一种实现依赖注入的方法是现代框架常用的方式,我这里的实现是简化了而已。把资源注册到一个容器,然后在用到时初始化资源。在具体的类中,并不直接依赖外部的类对象,所谓的解耦的一种手段。数据库适配器初始化延迟到了具体需要使用这个资源的时候,有时候程序的运行,不需要实例化数据库适配器,那么就不要初始化它(初始化数据库资源是昂贵的)。

———————————————————————————–

$adapter = new Zend\Db\Adapter\Adapter(array(
    'hostname' => 'localhost',
    'database' => 'test',
    'username' => 'root',
    'password' => '',
    
    'driver' => 'mysqli',
    'charset' =>'utf8'
    
    //'driver' => 'pdo_mysql',
    //'driver_options' => array(
    //    PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
    //),
));
$connection = $adapter->getDriver()->getConnection();
// connection还是具体数据库链接的抽象,它使用一个叫resource的名称代表最底层的数据库链接对象
// 通过getResource()获取这个数据库对象,然后可以调用具体的方法
$resource = $connection->getResource();
// 调用mysqli的具体方法
$resource->real_escape_string($);

以上程序为了能escape字符串而进行了不兼容调用,在Zend Framework新版本中,这个Bug已经修复,Zend\Db\Adapter\Platform\Mysql的quoteValue改为如下:

    public function quoteValue($value)
    {
        if ($this->resource instanceof DriverInterface) {
            $this->resource = $this->resource->getConnection()->getResource();
        }
        if ($this->resource instanceof \mysqli) {
            return '\'' . $this->resource->real_escape_string($value) . '\'';
        }
        if ($this->resource instanceof \PDO) {
            return $this->resource->quote($value);
        }
        return parent::quoteValue($value);
    }

Zend Framework 1.x Mysqli使用范例

由于项目需要,研究了一下Zend Framework 1.x Mysqli的驱动的使用(跟使用PDO的情况一样),大概浏览了一下Zend_Db包的源代码,同级别时代的实现,未发现可以超越Zend Framework的。原本打算自己来实现对应mysqli的封装的,大概研究了一下源代码,我就彻底放弃了。

set_include_path(implode(PATH_SEPARATOR, array(
    realpath(__DIR__ . '/../library'),
    get_include_path(),
)));

if(file_exists('../vendor/autoload.php')){
	$loader = include '../vendor/autoload.php';
}else{
	exit("Autoload Failed. ");
}
$loader->setUseIncludePath(true);

// $params = array(
// 		username	用户名		必须
// 		password	密码		必须
// 		dbname		数据库		必须

// 		port		端口号		
// 		socket		
// 		charset		字符集

// 		options = array(
// 				CASE_FOLDING				强制添加
// 				AUTO_QUOTE_IDENTIFIERS		强制添加
// 				FETCH_MODE					强制添加
// 				ALLOW_SERIALIZATION
// 				AUTO_RECONNECT_ON_UNSERIALIZE

// 		)
// 		driver_options = array()			参考mysqli_options()函数

// 		persistent	是否持久链接
// 		PROFILER	
// );

$params = array(
		'username' =>'root',
		'password' =>'',
		'dbname' =>'test',
		'charset' =>'utf8'
);
$db = Zend_Db::factory('MYSQLI', $params);
Zend_Db_Table_Abstract::setDefaultAdapter($db);

// 插入
$string = <<<'EOT'
'\'\" \\ \\ +
EOT;

for($i = 1; $i < 10; $i++){
	$db->insert('user', array('email'=>$string));
}

// 更新
$db->update('user', array('email'=>'xxxx'),'id=1');

// 删除
$db->delete('user','id=2');

// 查询
$result = $db->fetchAll("SELECT * FROM user");

print_r($result);
exit;