數(shù)據(jù)庫操作:基于NotORM的使用及優(yōu)化

2018-11-21 21:14 更新

有些裁縫只會塞塞襯里,修修補補,而有些裁縫卻能做出新東西--他們之間隔著一條深淵。 -- 《外套》

1.15.1 NotORM官網(wǎng)

這里使用了NotORM進行DB操作,具體的數(shù)據(jù)庫操作使用文檔請見NotORM官網(wǎng): http://www.notorm.com

(1) 基本CURD

//查詢
$row = DI()->notorm->user->where('id', 1)->fetch();

//更新
$data = array('name' => 'test', 'update_time' => time());
DI()->notorm->user->where('id', 1)->update($data);

//插入(須是同一個對象才能正確獲取插入的ID)
$data = array('name' => 'phalapi');
$userORM = DI()->notorm->user;
$userORM->insert($data);
$id = $userORM->insert_id();

//刪除
DI()->notorm->user->where('id', 1)->delete();

(2)update相同的數(shù)據(jù)的判斷

在使用update操作時,如果更新的數(shù)據(jù)和原來的一樣,則會返回0(影響0行)。這時,會和更新失敗(同樣影響0行)混淆。

但NotORM是一個優(yōu)秀的類庫,所以提供了優(yōu)秀的解決文案。我們在使用update時,只須了解這兩者返回的結(jié)果的微妙區(qū)別即可。
因為失敗異常時,返回false;而相同數(shù)據(jù)更新會返回0。即:

  • 1、update相同的數(shù)據(jù)時,返回0,嚴格來說是:int(0)
  • 2、update失敗時,如更新一個不存在的字段,返回false,即:bool(false)

用代碼表示,就是:

$rs = DI()->notorm->user->where('id', $userId)->update($data);

if ($rs >= 1) {
    //成功
} else if ($rs === 0) {
    //相同數(shù)據(jù),無更新
} else if ($rs === false) {
    //更新失敗
}

以下單元測試代碼,可以驗證上面的判斷:

    public function testUpdateOk()
    {
        $userId = 87;

        $rs = DI()->notorm->user->where('id', $userId)->update(array('reg_time' => time()));

        $this->assertSame(1, $rs);
    }

    public function testUpdateZero()
    {
        $userId = 1;

        $rs = DI()->notorm->user->where('id', $userId)->update(array('username' => 'aevit'));

        $this->assertSame(0, $rs);
    }

    public function testUpdateFail()
    {
        $userId = 1;

        $rs = DI()->notorm->user->where('id', $userId)->update(array('wrong_username' => 'aevit'));

        $this->assertSame(FALSE, $rs);
    }

(3)簡單的關(guān)聯(lián)查詢

如果是簡單的關(guān)聯(lián)查詢,可以使用NotORM支持的寫法,這樣的好處在于我們使用了一致的開發(fā),并且能讓PhalApi框架保持分布式的操作方式(注意,關(guān)聯(lián)的表仍然需要在同一個數(shù)據(jù)庫)。

以下是一個簡單的示例。

假設(shè)我們有這樣的數(shù)據(jù):

INSERT INTO `phalapi_user` VALUES ('1', 'wx_edebc877070133c65161d00799e00544', 'weixinName', '******', '4CHqOhe1Jxi3X9HmRfPOXygDnU267eCA', '1431790647', 'phpunit.png');
INSERT INTO `phalapi_user_session_0` VALUES ('1', '1', 'ABC', '', '0', '0', '0', null);

那么對應(yīng)關(guān)聯(lián)查詢的代碼如下面:

    public function testLeftJoin()
    {
        $rs = DI()->notorm->user_session_0
            ->select('expires_time, user.username, user.nickname')
            ->where('token', 'ABC')
            ->fetchRow();

        var_dump($rs);
    }

運行一下,我們可以看到這樣的輸出:

SELECT expires_time, user.username, user.nickname FROM phalapi_user_session_0 LEFT JOIN phalapi_user AS user ON phalapi_user_session_0.user_id = user.id WHERE (token = 'ABC') LIMIT 1;

.[1 - 0.06318s]SELECT expires_time, user.username, user.nickname FROM phalapi_user_session_0 LEFT JOIN phalapi_user AS user ON phalapi_user_session_0.user_id = user.id WHERE (token = 'ABC') LIMIT 1;<br>
array(3) {
  ["expires_time"]=>
  string(1) "0"
  ["username"]=>
  string(35) "wx_edebc877070133c65161d00799e00544"
  ["nickname"]=>
  string(10) "weixinName"
}

這樣,我們就可以實現(xiàn)關(guān)聯(lián)查詢的操作。按照NotORM官網(wǎng)的說法,則是:

If the dot notation is used for a column anywhere in the query ("$table.$column") then NotORM automatically creates left join to the referenced table. Even references across several tables are possible ("$table1.$table2.$column"). Referencing tables can be accessed by colon: $applications->select("COUNT(application_tag:tag_id)").

->select('expires_time, user.username, user.nickname')這一行調(diào)用將會【自動產(chǎn)生關(guān)聯(lián)操作】,而ON 的字段,則是這個字段關(guān)聯(lián)你配置的【表結(jié)構(gòu)】,外鍵默認為: 表名_id 。

(4)加1操作

NotORM已提供了NotORM_Literal,其用法如下:

DI()->notorm->user->where('id', 1)->update(array('age' => new NotORM_Literal("age + 1")));

當需要更新為當前時間,可以:

$array = array(
    "title" => "NotORM",
    "author_id" => null,
    "created" => new NotORM_Literal("NOW()"),
);

1.15.2 NotORM的優(yōu)化

但為了更符合項目的開發(fā),這里對NotORM的底層作了升級修改,以下為主要修改點和新的使用:

(1)將原來返回的結(jié)果全部從對象改成數(shù)組

對原來的大部分使用無特別影響,可按原來的方式開發(fā)。主要目的是為了更方面處理返回的數(shù)據(jù),以及簡化對結(jié)果的再解析,簡單明了。
如:

DI()->notorm->user->where('username = ?', 'dogstar')->fetch();

返回的將是一個數(shù)組:

array(7) {
  ["id"]=>
  string(3) "180"
  ["username"]=>
  string(17) "dogstar"
  ["regtime"]=>
  string(10) "1414811954"
  //...
}

(2)提供獲取全部結(jié)果的接口 - fetchAll() / fetchRows()

如:

$rows = DI()->notorm->event_picurl->where('eid', $eids)->fetchAll();

或:

$rows = DI()->notorm->event_picurl->where('eid', $eids)->fetchRows();

即可獲取全部的數(shù)據(jù),不再受限于分頁。
這里提供了fetchAll()和fetchRows()兩種等效的操作,是為了減少記憶的痛苦,下同。

(3)提供更靈活的查詢方式 - queryAll() / queryRows()

當需要進行復(fù)雜的SQL查詢時,可以使用此接口,如:
(注意:limit替換值:start和:num必須使用int類型)

$sql = 'select * from example AS ep LEFT JOIN user AS u ON ep.ui
d = u.id  where ep.touid = :userId ORDER BY dateline desc LIMIT :start,:num';
$params = array(':userId' => $userId, ':start' => $start, ':num' => $num);
$rs= DI()->notorm->example->queryAll($sql, $params);

或:

$rs= DI()->notorm->example->queryRows($sql, $params);

(4)limit 操作的調(diào)整

取消了NotORM中對OFFSET關(guān)鍵字的使用,改用逗號的寫法,修改后正確的使用方法應(yīng)該是:

$table->limit(10);  // limit 10   # 查詢前10個

$table->limit(5, 10); // limit 5,10   # 從第5個位置開始,查詢前10個

(5)禁止全表刪除,防止誤刪

出于對數(shù)據(jù)的保護,當執(zhí)行刪除操作卻又沒有任何where條件時,將會禁止進行全表操作。如:

    public function testDeleteAll()
    {
        DI()->notorm->user->delete();
    }

可以看到:

$ phpunit --filter testDeleteAll ./Api/Api_User_Test.php 
PHPUnit 4.3.4 by Sebastian Bergmann.

E

Time: 315 ms, Memory: 6.25Mb

There was 1 error:

1) PhpUnderControl_ApiUser_Test::testDeleteAll
Exception: sorry, you can not delete the whole table --dogstar

(6)添加& __sql__ =1請求參數(shù),可開啟HTTP調(diào)試模式

當處于debug模式時,可以輸入執(zhí)行的全部SQL語句,以便調(diào)試。

如:

SELECT times FROM tpl_user_session_10 WHERE (user_id = ?); -- '74110'
{"ret":0,"data":{"code":0},"msg":""}

(7)關(guān)于NotORM中fetch()操作沒有l(wèi)imit 1的處理方案 - fetchOne() / fetchRow()

之前,有開發(fā)同學提及到,為什么notorm的基類fetch為啥沒用limit(1)呢。后來,我去發(fā)了下NotORM的寫法,確實做得很微妙。
其實NotORM之所以沒有在fetch()里面自動limit 1是因為,你可以循環(huán)地獲取數(shù)據(jù),如:

$user = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->limit(3);
while(($row = $user->fetch())) {
    var_dump($row);
 }

但是,更多情況下,我們只需要獲取某一行的數(shù)據(jù),上面的做法會造成不必要的SQL查詢。為了保留原來的寫法,我特意添加擴展了一個新的操作:fetchRow(),用法同fetch(),但只會取第一條。
以下是使用示例:

$rs = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->fetchRow());

var_dump($rs);

//結(jié)果輸出類如:
array(3) {
  ["id"]=>
  string(1) "1"
  ["username"]=>
  string(5) "aevit"
  ["nickname"]=>
  string(4) "test"
}

//對應(yīng)執(zhí)行的SQL語句:
[2 - 0.06544s]SELECT id, username, nickname FROM fami_user WHERE (id > ?) LIMIT 1; -- 0<br>

如果,我們只需要獲取這一行的某個字段,也可以像fecth()那樣使用,即:

$rs = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->fetchRow('nickname'));

var_dump($rs);

//結(jié)果輸出類如:
string(4) "test"

//紀錄不存在時,返回 bool(false)

(8)顯式的SQL語法異常提示

很多時候,在開發(fā)時,我們對數(shù)據(jù)庫操作一開始會存在一些SQL語法的問題,PDO會返回false,且原來NotORM也是使用 靜默方式 來處理這類錯誤,從而使得開發(fā)人員有時難以發(fā)現(xiàn)這些問題,除非將調(diào)試的SQL手動放到數(shù)據(jù)庫執(zhí)行才能發(fā)現(xiàn)問題所在。

為了能給開發(fā)同學更早、更直觀的方式查看問題的所在,這里對NotORM底層進行了調(diào)整,使用了 顯式方式 的策略來處理,即:直接拋出PDO異常。

如:

$userId = 1;

//OK
$rs = DI()->notorm->user->select('username')->where('id', $userId)->fetchOne();

//WRONG
$rs = DI()->notorm->user->select('wrong_username')->where('id', $userId)->fetchOne();

將會看到:

[1 - 0.06437s]SELECT username FROM fami_user WHERE (id = 1) LIMIT 1;<br>
[2 - 0.06496s]SELECT wrong_username FROM fami_user WHERE (id = 1) LIMIT 1;<br>

PDOException: Unknown column 'wrong_username' in 'field list'

(9)復(fù)雜的關(guān)聯(lián)查詢

如果是復(fù)雜的關(guān)聯(lián)查詢,則是建議使用原生態(tài)的SQL語句,但我們?nèi)匀豢梢员3趾芎玫膶懛?,如這樣一個示例:

        $sql = 'SELECT t.id, t.team_name, v.vote_num '
            . 'FROM phalapi_team AS t LEFT JOIN phalapi_vote AS v '
            . 'ON t.id = v.team_id '
            . 'ORDER BY v.vote_num DESC';
        $rows = $this->getORM()->queryAll($sql, array());

注意,此時的表需要使用全名,即自帶前綴。這樣也可以實現(xiàn)更自由的關(guān)聯(lián)查詢。

(10)事務(wù)操作

關(guān)于事務(wù)操作,可以參考 NotORM官網(wǎng) 的說明:

$db->transaction = $command Assign 'BEGIN', 'COMMIT' or 'ROLLBACK' to start or stop transaction 

即:

//第一步:先指定待進行事務(wù)的數(shù)據(jù)庫(通過獲取一個notorm表實例來指定;否則會提示:PDO There is no active transaction)
$user = DI()->notorm->user;

//第二步:開啟事務(wù)開關(guān)(此開關(guān)會將當前全部打開的數(shù)據(jù)庫都進行此設(shè)置)
DI()->notorm->transaction = 'BEGIN';

//第三步:進行數(shù)據(jù)庫操作
$user->insert(array('name' => 'test1',));
$user->insert(array('name' => 'test2',));

//第四:提交/回滾
DI()->notorm->transaction = 'COMMIT';
//DI()->notorm->transaction = 'ROLLBACK';

推薦使用PhalApi的事務(wù)操作方式

PhalApi一開始對事務(wù)這塊考慮不周,后來發(fā)現(xiàn)很多同學、很多項目都需要用到數(shù)據(jù)庫事務(wù)操作。
基于此,在不破壞原來的代碼基礎(chǔ)上,我們決定在PhalApi_DB_NotORM上添加對數(shù)據(jù)庫維度的事務(wù)操作支持。

示例簡單如下:

    public function testTransactionCommit()
    {
        //Step 1: 開啟事務(wù)
        $this->notorm->beginTransaction('db_demo');

        //Step 2: 數(shù)據(jù)庫操作
        $this->notorm->user>insert(array('name' => 'test1'));
        $this->notorm->user>insert(array('name' => 'test2'));

        //Step 3: 提交事務(wù)
        $this->notorm->commit('db_demo');

    }

溫馨提示: 以上操作,須PhalApi 1.3.1 及以上版本才能支持。

(11)擴展對非MySQL數(shù)據(jù)庫的支持

PhalApi使用的是NotORM來進行數(shù)據(jù)庫操作,而NotORM底層則是采用了PDO。目前,NotORM支持: MySQL, SQLite, PostgreSQL, MS SQL, Oracle (Dibi support is obsolete)。

但需要注意的是,PhalApi本身對NotORM進行了修改,需要調(diào)整一下代碼才能更好地支持除MySQL外的數(shù)據(jù)庫。即使NotORM不支持的數(shù)據(jù)庫,你也可以輕松通過添加擴展的方式來支持。如:

首先,定制自己的數(shù)據(jù)庫連接的PDO。

class Common_MyDB extends PhalApi_DB_NotORM {

    protected function createPDOBy($dbCfg) {
        /* Connect to an ODBC database using driver invocation */
    $dsn = 'uri:file:///usr/local/dbconnect';
    return new PDO($dsn, $dbCfg['user'], $dbCfg['password']);
    }
}

隨后,在初始化文件init.php中重新注冊DI()->notorm即可,如:

//數(shù)據(jù)操作 - 基于NotORM,$_GET['__sql__']可自行改名
DI()->notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new Common_MyDB(DI()->config->get('dbs'), $debug);
};

1.15.3 可選的Model基類

(1)表數(shù)據(jù)入口模式

我們一直在考慮,是否應(yīng)該提供數(shù)據(jù)庫的基本操作支持,以減少開發(fā)人員重復(fù)手工編寫基本的數(shù)據(jù)操作。

最后,我們認為是需要的。然后就引發(fā)了新的問題:是以繼承還是以委托來支持?

委托有助于降低繼承的層級,但仍然需要編寫同類的操作然后再次委托。所以,這里提供了基于NotORM的Model基類:PhalApi_Model_NotORM。

然而提供這個基類還是會遇到一些問題,例如:如何界定基本操作?如何處理分表存儲?如何支持定制化?

由于我們這里的Model使用了 “表數(shù)據(jù)入口” 模式,而不是“行數(shù)據(jù)入口”,也不是“活動紀錄”,也不是復(fù)雜的“數(shù)據(jù)映射器”。所以在使用時可以考慮是否需要此基類。即使這樣,你也可以很輕松轉(zhuǎn)換到“行數(shù)據(jù)入口”和“活動紀錄”模式。這里,PhalApi中的Model是更廣義上的數(shù)據(jù)源層(后面會有更多說明),因此對應(yīng)地PhalApi_Model_NotORM基類充當了數(shù)據(jù)庫表訪問入口的對象,處理表中所有的行。

(2)規(guī)約層的CURD

在明白了Model基類的背景后,再來了解其具體的操作和如何繼承會更有意義。

而具體的操作則與數(shù)據(jù)表的結(jié)構(gòu)相關(guān),在“約定編程”下:即每一個表都有一個主鍵(通常為id,也可以自由配置)以及一個序列化LOB字段ext_data。我們很容易想到Model接口的定義(注釋已移除,感興趣的同學可查看源碼):

interface PhalApi_Model {

    public function get($id, $fields = '*');

    public function insert($data, $id = NULL);

    public function update($id, $data);

    public function delete($id);
}

上面的接口在規(guī)約層上提供了基于表主鍵的CURD基本操作,在具體實現(xiàn)時,需要注意兩點:一是分表的處理;另一點則是LOB字段的序列化。

(3)不使用Model基類的寫法

由于我們使用了NotORM進行數(shù)據(jù)庫的操作,所以這里也提供了基于NotORM的基類:PhalApi_Model_NotORM。下面以我們熟悉的獲取用戶的基本信息為例,說明此基類的使用。

為喚醒記憶,下面貼上Model_User類原來的代碼:

// $ vim ./Demo/Model/User.php

<?php

class Model_User {

    public function getByUserId($userId) {
        return DI()->notorm->user->select('*')->where('id = ?', $userId)->fetch();
    }
}

對應(yīng)的調(diào)用:

$model = new Model_User();
$rs = $model->getByUserId($userId);

(4)繼承Model基類的寫法

若繼承于PhalApi_Model_NotORM,則是:

// $ vim ./Demo/Model/User.php

<?php

class Model_User extends PhalApi_Model_NotORM {

    protected function getTableName($id) {
        return 'user';
    }
}

從上面的代碼可以看出,基類已經(jīng)提供了基于主鍵的CURD操作,但我們需要鉤子函數(shù)以返回對應(yīng)的表名。相應(yīng)地,外部調(diào)用則調(diào)整為:

$model = new Model_User();
$rs = $model->get($userId);

再進一步,我們可以得到其他的基本操作:

$model = new Model_User();

//查詢
$row = $model->get(1);
$row = $model->get(1, 'id, name'); //取指定的字段
$row = $model->get(1, array('id', 'name')); //可以數(shù)組取指定要獲取的字段

//更新
$data = array('name' => 'test', 'update_time' => time());
$model->update(1, $data); //基于主鍵的快速更新

//插入
$data = array('name' => 'phalapi');
$id = $model->insert($data);
//$id = $model->insert($data, 5); //如果是分表,可以這樣指定

//刪除
$model->delete(1);

1.15.4 定制化你的Model基類

正如上面提及到的兩個問題:LOB序列化和分表處理。所以,如果PhalApi現(xiàn)有就此兩問題的解決方案不能滿足項目的需求,可作定制化處理。

(1)LOB序列化

先是LOB序列化,考慮到有分表的存在,當發(fā)生數(shù)據(jù)庫變更時(特別在線上環(huán)境)會有一定的難度和風險,因此引入了擴展字段ext_data。當然,此字段也應(yīng)對數(shù)據(jù)庫變更的同時,也可以作為簡單明了的值對象的大對象。序列化LOB首先要考慮的問題是使用二進制(BLOB)還是文本(CLOB),出于通用性、易讀性和測試性,我們目前使用了json格式的文本序列化。所以,如果考慮到空間或性能問題(在少量數(shù)據(jù)下我認為問題不大,如果數(shù)據(jù)量大,應(yīng)該及時重新調(diào)整數(shù)據(jù)庫表結(jié)構(gòu)),可以重寫formatExtData() & parseExtData()。

如改成serialize序列化:

abstract class Common_Model_NotORM extends PhalApi_Model_NotORM {

    /**
     * 對LOB的ext_data字段進行格式化(序列化)
     */
    protected function formatExtData(&$data) {
        if (isset($data['ext_data'])) {
            $data['ext_data'] = serialize($data['ext_data']);
        }
    }

    /**
     * 對LOB的ext_data字段進行解析(反序列化)
     */
    protected function parseExtData(&$data) {
        if (isset($data['ext_data'])) {
            $data['ext_data'] = unserialize($data['ext_data'], true);
        }
    }

    // ...
}

將Model類繼承于Common_Model_NotORM后,

// $ vim ./Demo/Model/User.php

<?php
class Model_User extends Common_Model_NotORM {
   //...
}

就可以輕松切換到序列化,如:

$model = new Model_User();

//帶有ext_data的更新
$extData = array('level' => 3, 'coins' => 256);
$data = array('name' => 'test', 'update_time' => time(), 'ext_data' => $extData);
$model->update(1, $data); //基于主鍵的快速更新

(2)分表處理

其次是分表處理,同樣考慮到分表的情況,以及不同的表可能配置不同的主鍵表,而基于主鍵的CURD又必須要先知道表的主鍵名才能進行SQL查詢。所以,問題就演變成了如何找到表的主鍵名。這里可以自動匹配,也可以手工指定。自動匹配是智能的,因為當我們更改表的主鍵時,可以自動同步更新而不需要擔心遺漏(雖然這種情況很少發(fā)生)。手工指定可以大大減少系統(tǒng)不必要的匹配操作,因為我們開發(fā)人員也知道數(shù)據(jù)庫的主鍵名是什么,但需要手工編寫一些代碼。在這里,提供了可選的手工指定,即可重寫getTableKey($table)來指定你的主鍵名。

如,當我們的表的主鍵都固定為id時:

abstract class Common_Model_NotORM extends PhalApi_Model_NotORM {

    protected function getTableKey($table) {
        return 'id';
    }
}

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號