# Model數據模型層與數據庫操作
Model層稱為數據模型層,負責技術層面上對數據信息的提取、存儲、更新和刪除等操作,數據可來自內存,也可以來自持久化存儲媒介,甚至可以是來自外部第三方系統。雖然PhalApi的Model層是廣義上的數據層,但考慮到大部分數據都是來自于數據庫的操作,所以這一章將重點講述如何進行數據庫操作。
## 數據庫配置
數據庫的配置文件為./config/dbs.php,默認使用的是MySQL數據庫,所以需要配置MySQL的連接信息。servers選項用于配置數據庫服務器相關信息,可以配置多組數據庫實例,每組包括數據庫的賬號、密碼、數據庫名字等信息。不同的數據庫實例,使用不同標識作為下標。
servers數據庫配置項|說明
---|---
host|數據庫域名
name|數據庫名字
user|數據庫用戶名
password|數據庫密碼
port|數據庫端口
charset|數據庫字符集
tables選項用于配置數據庫表的表前綴、主鍵字段和路由映射關系,可以配置多個表,下標為不帶表前綴的表名,其中```__default__```下標選項為缺省的數據庫路由,即未配置的數據庫表將使用這一份默認配置。
表2-12 表配置項
tables表配置項|說明
---|---
prefix|表前綴
key|表主鍵
map|數據庫實例映射關系,可配置多組。每組格式為:```array('db' => 服務器標識, 'start' => 開始分表標識, 'end' => 結束分表標識)```,start和end要么都不提供,要么都提供
例如默認數據庫配置為:
```php
return array(
/**
* DB數據庫服務器集群
*/
'servers' => array(
'db_master' => array( //服務器標記
'host' => '127.0.0.1', //數據庫域名
'name' => 'phalapi', //數據庫名字
'user' => 'root', //數據庫用戶名
'password' => '', //數據庫密碼
'port' => 3306, //數據庫端口
'charset' => 'UTF8', //數據庫字符集
),
),
/**
* 自定義路由表
*/
'tables' => array(
//通用路由
'__default__' => array(
'prefix' => 'tbl_',
'key' => 'id',
'map' => array(
array('db' => 'db_master'),
),
),
),
);
```
其中,在servers中配置了名稱為db_master數據庫實例,意為數據庫主庫,其host為localhost,名稱為phalapi,用戶名為root等。在tables中,只配置了通用路由,并且表前綴為tbl_,主鍵均為id,并且全部使用db_demo數據庫實例。
> **溫馨提示:**當tables中配置的db數據庫實例不存在servers中時,將會提示數據庫配置錯誤。
### 如何排查數據庫連接錯誤?
普通情況下,數據庫連接失敗時會這樣提示:
```
{
"ret": 500,
"data": [],
"msg": "服務器運行錯誤: 數據庫db_demo連接失敗"
}
```
考慮到生產環境不方便爆露服務器的相關信息,故這樣簡化提示。當在開發過程中,需要定位數據庫連接失敗的原因時,可使用debug調試模式。開啟調試后,當再次失敗時,會看到類似這樣的提示:
```
{
"ret": 500,
"data": [],
"msg": "服務器運行錯誤: 數據庫db_demo連接失敗,異常碼:1045,錯誤原因:SQLSTATE[28000] [1045] ... ..."
}
```
然后,便可根據具體的錯誤提示進行排查解決。
## NotORM簡介
NotORM是一個優秀的開源PHP類庫,可用于操作數據庫。PhalApi的數據庫操作,主要是依賴此NotORM來完成。
> 參考:NotORM官網:[www.notorm.com](http://www.notorm.com/)。
所以,如果了解NotORM的使用,自然而然對PhalApi中的數據庫操作也就一目了然了。但為了更符合接口類項目的開發,PhalApi對NotORM的底層進行優化和調整。以下改動點包括但不限于:
+ 將原來返回的結果全部從對象類型改成數組類型,便于數據流通
+ 添加查詢多條紀錄的接口:```NotORM_Result::fetchAll()```和```NotORM_Result::fetchRows()```
+ 添加支持原生SQL語句查詢的接口:```NotORM_Result::queryAll()```和```NotORM_Result::queryRows()```
+ limit 操作的調整,取消原來OFFSET關鍵字的使用
+ 當數據庫操作失敗時,拋出PDOException異常
+ 將結果集中以主鍵作為下標改為以順序索引作為下標
+ 禁止全表刪除,防止誤刪
+ 調整調試模式
### 如何獲取NotORM實例?
在PhalApi中獲取NotORM實例,有兩種方式:全局獲取方式、局部獲取方式。
+ **全局獲取**:在任何地方,使用DI容器中的全局notorm服務:```\PhalApi\DI()->notorm```
+ **局部獲取**:在繼承PhalApi\Model\NotORMModel的子類中使用:```$this->getORM()```
第一種全局獲取的方式,可以用于任何地方,這是因為我們已經在初始化文件中注冊了```\PhalApi\DI()->notorm```這一服務。
第二種局部獲取的方式,僅限用于繼承PhalApi\Model\NotORMModel的子類中。首先需要實現相應的Model子類,通常一個表對應一個Model子類。例如為user表創建相應的Model類。
PhalApi推薦使用封裝的第二種方式,并且下面所介紹的使用都是基于第二種快速方式。以下是獲取一個NotORM實例的示例。
```php
class User extends NotORM {
public function doSth() {
$orm = $this->getORM();
}
}
```
## Model子類與表名
### 如何新增一個Model類?
通常情況下,一張表對應一個Model類。當需要新增時,可以繼承于PhalApi\Model\NotORMModel類,并放置在App\Model命名空間下。例如,對于數據庫表tbl_user:
```sql
CREATE TABLE `tbl_user` (
`id` int(11) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`age` int(3) DEFAULT NULL,
`note` varchar(45) DEFAULT NULL,
`create_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
可以新增App\Model\User.php文件,并在里面放置以下代碼。
```php
<?php
namespace App\Model;
use PhalApi\Model\NotORMModel as NotORM;
class User extends NotORM {
}
```
至此,便可得到一個基本的Model子類了。
#### Model的基本操作
對于基本的Model子類,可以得到基本的數據庫操作。以下示例演示了Model的基本CURD操作。
```php
$model = new App\Model\User();
// 查詢
$row = $model->get(1);
$row = $model->get(1, 'id, name'); //取指定的字段
$row = $model->get(1, array('id', 'name')); //可以數組取指定要獲取的字段
// 更新
$data = array('name' => 'test', 'update_time' => time());
$model->update(1, $data); //基于主鍵的快速更新
// 插入
$data = array('name' => 'phalapi');
$id = $model->insert($data);
//$id = $model->insert($data, 5); //如果是分表,可以通過第二個參數指定分表的參考ID
// 刪除
$model->delete(1);
```
#### 數據庫表名配置
上面的App\Model\User類,自動匹配的表名為:user,加上配置前綴“tbl_”,完整的表名是:tbl_usre。默認表名的自動匹配規則是:取“\Model\”后面部分的字符全部轉小寫,并且在轉化后會加上配置的表前綴。
雙如:
```php
<?php
namespace App\Model\User;
use PhalApi\Model\NotORMModel as Model;
class Friends extends Model {
}
```
則類App\Model\User\Friends自動匹配的表名為```user_friends```。以下是2.x版本的一些示例:
2.x 的Model類名|對應的文件|自動匹配的表名
---|---|---
App\Model\User|./src/app/Model/User.php|user
App\ModelUser\Friends|./src/app/Model/User/Friends.php|user_friends
App\User\Model\Friends|./src/app/user/Model/Friends.php|friends
App\User\Model\User\Friends|./src/app/user/Model/User/Friends.php|user_friends
但在以下場景或者其他需要手動指定表名的情況,可以重寫```PhalApi\Model\NotORMModel::getTableName($id)```方法并手動指定表名。
+ 存在分表
+ Model類名不含有“Model_”
+ 自動匹配的表名與實際表名不符
+ 數據庫表使用蛇形命名法而類名使用大寫字母分割的方式
如,當Model_User類對應的表名為:my_user表時,可這樣重新指定表名:
```php
<?php
namespace App\Model;
use PhalApi\Model\NotORMModel as NotORM;
class User extends NotORM {
protected function getTableName($id) {
return 'my_user';
}
}
```
其中,$id參數用于進行分表的參考主鍵,只有當存在分表時才需要用到。通常傳入的$id是整數,然后對分表的總數進行求余從而得出分表標識。
即存在分表時,需要返回的格式為:表名稱 + 下劃線 + 分表標識。分表標識通常從0開始,為連續的自然數。
## 在Model內的CURD基本操作
假設對于前面的tbl_user表,有以下數據。
```sql
INSERT INTO `tbl_user` VALUES ('1', 'dogstar', '18', 'oschina', '2015-12-01 09:42:31');
INSERT INTO `tbl_user` VALUES ('2', 'Tom', '21', 'USA', '2015-12-08 09:42:38');
INSERT INTO `tbl_user` VALUES ('3', 'King', '100', 'game', '2015-12-23 09:42:42');
```
下面將結合示例,分別介紹NotORM更為豐富的數據庫操作。在開始之前,假定已有:
```php
class User extends NotORM {
public function test() {
$user = $this->getORM();
}
}
```
#### SQL基本語句介紹
+ **SELECT字段選擇**
選擇單個字段:
```php
// SELECT id FROM `tbl_user`
$user->select('id')
```
選擇多個字段:
```php
// SELECT id, name, age FROM `tbl_user`
$user->select('id, name, age')
```
使用字段別名:
```php
// SELECT id, name, MAX(age) AS max_age FROM `tbl_user`
$user->select('id, name, MAX(age) AS max_age')
```
選擇全部表字段:
```php
// SELECT * FROM `tbl_user`
$user->select('*')
```
+ **WHERE條件**
單個條件:
```php
// WHERE id = 1
$user->where('id', 1)
$user->where('id = ?', 1)
$user->where(array('id', 1))
```
多個AND條件:
```php
// WHERE id > 1 AND age > 18
$user->where('id > ?', 1)->where('age > ?', 18)
$user->and('id > ?', 1)->and('age > ?', 18)
$user->where('id > ? AND age > ?', 1, 18)
$user->where(array('id > ?' => 1, 'age > ?' => 10))
// WHERE name = 'dogstar' AND age = 18
$user->where(array('name' => 'dogstar', 'age' => 18))
```
多個OR條件:
```php
// WHERE name = 'dogstar' OR age = 18
$user->or('name', 'dogstar')->or('age', 18)
```
嵌套條件:
```php
// WHERE ((name = ? OR id = ?)) AND (note = ?) -- 'dogstar', '1', 'xxx'
// 實現方式1:使用AND拼接
$user->where('(name = ? OR id = ?)', 'dogstar', '1')->and('note = ?', 'xxx')
// 實現方式2:使用WHERE,并順序傳遞多個參數
$user->where('(name = ? OR id = ?) AND note = ?', 'dogstar', '1', 'xxx')
// 實現方式3:使用WHERE,并使用一個索引數組順序傳遞參數
$user->where('(name = ? OR id = ?) AND note = ?', array('dogstar', '1', 'xxx'))
// 實現方式4:使用WHERE,并使用一個關聯數組傳遞參數
$user->where('(name = :name OR id = :id) AND note = :note',
array(':name' => 'dogstar', ':id' => '1', ':note' => 'xxx'))
```
IN查詢:
```php
// WHERE id IN (1, 2, 3)
$user->where('id', array(1, 2, 3))
// WHERE id NOT IN (1, 2, 3)
$user->where('NOT id', array(1, 2, 3))
// WHERE (id, age) IN ((1, 18), (2, 20))
$user->where('(id, age)', array(array(1, 18), array(2, 20)))
```
模糊匹配查詢:
```php
// WHERE name LIKE '%dog%'
$user->where('name LIKE ?', '%dog%')
// WHERE name NOT LIKE '%dog%'
$user->where('name NOT LIKE ?', '%dog%')
```
> **溫馨提示:**需要模糊匹配時,不可寫成:where('name LIKE %?%', 'dog')。
NULL判斷查詢:
```php
// WHERE (name IS NULL)
$user->where('name', null)
```
非NULL判斷查詢:
```php
// WHERE (name IS NOT ?) LIMIT 1; -- NULL
$user->where('name IS NOT ?', null)
```
+ **ORDER BY排序**
單個字段升序排序:
```php
// ORDER BY age
$user->order('age')
$user->order('age ASC')
```
單個字段降序排序:
```php
// ORDER BY age DESC
$user->order('age DESC')
```
多個字段排序:
```php
// ORDER BY id, age DESC
$user->order('id')->order('age DESC')
$user->order('id, age DESC')
```
+ **LIMIT數量限制**
限制數量,如查詢前10個:
```php
// LIMIT 10
$user->limit(10)
```
分頁限制,如從第5個位置開始,查詢前10個:
```php
// LIMIT 5, 10
$user->limit(5, 10)
```
+ **GROUP BY和HAVING**
只有GROUP BY,沒有HAVING:
```php
// GROUP BY note
$user->group('note')
```
既有GROUP BY,又有HAVING:
```php
// GROUP BY note HAVING age > 10
$user->group('note', 'age > 10')
```
#### CURD之插入操作
插入操作可分為插入單條紀錄、多條紀錄,或根據條件插入。
操作|說明|示例|備注|是否PhalApi新增
---|---|---|---|---
insert()|插入數據|```$user->insert($data);```|全局方式需要再調用insert_id()獲取插入的ID|否
insert_multi()|批量插入|```$user->insert_multi($rows);```|可批量插入|否
insert_update()|插入/更新|接口簽名:```insert_update(array $unique, array $insert, array $update = array()```|不存時插入,存在時更新|否
插入單條紀錄數據,注意,必須是保持狀態的同一個NotORM表實例,方能獲取到新插入的行ID,且表必須設置了自增主鍵ID。
```php
// INSERT INTO tbl_user (name, age, note) VALUES ('PhalApi', 1, 'framework')
$data = array('name' => 'PhalApi', 'age' => 1, 'note' => 'framework');
$user->insert($data);
$id = $user->insert_id();
var_dump($id);
// 輸出:新增的ID
int (4)
// 或者使用Model封裝的insert()方法
$model = new Model_User();
$id = $model->insert($data);
var_dump($id);
```
批量插入多條紀錄數據:
```php
// INSERT INTO tbl_user (name, age, note) VALUES ('A君', 12, 'AA'), ('B君', 14, 'BB'), ('C君', 16, 'CC')
$rows = array(
array('name' => 'A君', 'age' => 12, 'note' => 'AA'),
array('name' => 'B君', 'age' => 14, 'note' => 'BB'),
array('name' => 'C君', 'age' => 16, 'note' => 'CC'),
);
$rs = $user->insert_multi($rows);
var_dump($rs);
// 輸出,成功插入的條數
int(3)
```
插入/更新:
```php
// INSERT INTO tbl_user (id, name, age, note) VALUES (8, 'PhalApi', 1, 'framework')
// ON DUPLICATE KEY UPDATE age = 2
$unique = array('id' => 8);
$insert = array('id' => 8, 'name' => 'PhalApi', 'age' => 1, 'note' => 'framework');
$update = array('age' => 2);
$rs = $user->insert_update($unique, $insert, $update);
var_dump($rs);
// 輸出影響的行數
```
#### CURD之更新操作
操作|說明|示例|備注|是否PhalApi新增
---|---|---|---|---
update()|更新數據|```$user->where('id', 1)->update($data);```|更新異常時返回false,數據無變化時返回0,成功更新返回1|否
根據條件更新數據:
```php
// UPDATE tbl_user SET age = 2 WHERE (name = 'PhalApi');
$data = array('age' => 2);
$rs = $user->where('name', 'PhalApi')->update($data);
var_dump($rs);
// 輸出
int(1) //正常影響的行數
int(0) //無更新,或者數據沒變化
boolean(false) //更新異常、失敗
```
在使用update()進行更新操作時,如果更新的數據和原來的一樣,則會返回0(表示影響0行)。這時,會和更新失敗(同樣影響0行)混淆。但NotORM是一個優秀的類庫,它已經提供了優秀的解決文案。我們在使用update()時,只須了解這兩者返回結果的微妙區別即可。因為失敗異常時,返回false;而相同數據更新會返回0。即:
+ 1、更新相同的數據時,返回0,嚴格來說是:int(0)
+ 2、更新失敗時,如更新一個不存在的字段,返回false,即:bool(false)
用代碼表示,就是:
```php
$rs = DI()->notorm->user->where('id', $userId)->update($data);
if ($rs >= 1) {
// 成功
} else if ($rs === 0) {
// 相同數據,無更新
} else if ($rs === false) {
// 更新失敗
}
```
更新數據,進行加1操作:
```php
// UPDATE tbl_user SET age = age + 1 WHERE (name = 'PhalApi')
$rs = $user->where('name', 'PhalApi')->update(array('age' => new NotORM_Literal("age + 1")));
var_dump($rs);
// 輸出影響的行數
```
#### CURD之查詢操作
查詢操作主要有獲取一條紀錄、獲取多條紀錄以及聚合查詢等。
操作|說明|示例|備注|是否PhalApi新增
---|---|---|---|---
fetch()|循環獲取每一行|```while($row = $user->fetch()) { ... ... }```||否
fetchOne()|只獲取第一行|```$row = $user->where('id', 1)->fetchOne();```|等效于fetchRow()|是
fetchRow()|只獲取第一行|```$row = $user->where('id', 1)->fetchRow();```|等效于fetchOne()|是
fetchPairs()|獲取鍵值對|```$row = $user->fetchPairs('id', 'name');```|第二個參數為空時,可取多個值,并且多條紀錄|否
fetchAll()|獲取全部的行|```$rows = $user->where('id', array(1, 2, 3))->fetchAll();```|等效于fetchRows()|是
fetchRows()|獲取全部的行|```$rows = $user->where('id', array(1, 2, 3))->fetchRows();```|等效于fetchAll()|是
queryAll()|復雜查詢下獲取全部的行,默認下以主鍵為下標|```$rows = $user->queryAll($sql, $parmas);```|等效于queryRows()|是
queryRows()|復雜查詢下獲取全部的行,默認下以主鍵為下標|```$rows = $user->queryRows($sql, $parmas);```|等效于queryAll()|是
count()|查詢總數|```$total = $user->count('id');```|第一參數可省略|否
min()|取最小值|```$minId = $user->min('id');```||否
max()|取最大值|```$maxId = $user->max('id');```||否
sum()|計算總和|```$sum = $user->sum('age');```||否
循環獲取每一行,并且同時獲取多個字段:
```php
// SELECT id, name FROM tbl_user WHERE (age > 18);
$user = $user->select('id, name')->where('age > 18');
while ($row = $user->fetch()) {
var_dump($row);
}
// 輸出
array(2) {
["id"]=>
string(1) "2"
["name"]=>
string(3) "Tom"
}
array(2) {
["id"]=>
string(1) "3"
["name"]=>
string(4) "King"
}
... ...
```
循環獲取每一行,并且只獲取單個字段。需要注意的是,指定獲取的字段,必須出現在select里,并且返回的不是數組,而是字符串。
```php
// SELECT id, name FROM tbl_user WHERE (age > 18);
$user = $user->select('id, name')->where('age > 18');
while ($row = $user->fetch('name')) {
var_dump($row);
}
// 輸出
string(3) "Tom"
string(4) "King"
... ...
```
注意!以下是錯誤的用法。還記得前面所學的NotORM狀態的保持嗎?因為這里每次循環都會新建一個NotORM表實例,所以沒有保持前面的查詢狀態,從而死循環。
```php
while ($row = DI()->notorm->user->select('id, name')->where('age > 18')->fetch('name')) {
var_dump($row);
}
```
只獲取第一行,并且獲取多個字段,等同于fetchRow()操作。
```php
// SELECT id, name FROM tbl_user WHERE (age > 18) LIMIT 1;
$rs = $user->select('id, name')->where('age > 18')->fetchOne();
var_dump($rs);
// 輸出
array(2) {
["id"]=>
string(1) "2"
["name"]=>
string(3) "Tom"
}
```
只獲取第一行,并且只獲取單個字段,等同于fetchRow()操作。
```php
var_dump($user->fetchOne('name'));
// 輸出
string(3) "Tom"
```
獲取鍵值對,并且獲取多個字段:
```
// SELECT id, name, age FROM tbl_user LIMIT 2;
$rs = $user->select('name, age')->limit(2)->fetchPairs('id'); //指定以ID為KEY
var_dump($rs);
// 輸出
array(2) {
[1]=>
array(3) {
["id"]=>
string(1) "1"
["name"]=>
string(7) "dogstar"
["age"]=>
string(2) "18"
}
[2]=>
array(3) {
["id"]=>
string(1) "2"
["name"]=>
string(3) "Tom"
["age"]=>
string(2) "21"
}
}
```
獲取鍵值對,并且只獲取單個字段。注意,這時的值不是數組,而是字符串。
```php
// SELECT id, name FROM tbl_user LIMIT 2
var_dump($user->limit(2)->fetchPairs('id', 'name')); //通過第二個參數,指定VALUE的列
// 輸出
array(2) {
[1]=>
string(7) "dogstar"
[2]=>
string(3) "Tom"
}
```
獲取全部的行,相當于fetchRows()操作。
```php
// SELECT * FROM tbl_user
var_dump($user->fetchAll());
// 輸出全部表數據,結果略
```
使用原生SQL語句進行查詢,并獲取全部的行:
```php
// SELECT name FROM tbl_user WHERE age > :age LIMIT 1
$sql = 'SELECT name FROM tbl_user WHERE age > :age LIMIT 1';
$params = array(':age' => 18);
$rs = $user->queryAll($sql, $params);
var_dump($rs);
// 輸出
array(1) {
[0]=>
array(1) {
["name"]=>
string(3) "Tom"
}
}
// 除了使用上面的關聯數組傳遞參數,也可以使用索引數組傳遞參數
$sql = 'SELECT name FROM tbl_user WHERE age > ? LIMIT 1';
$params = array(18);
// 也使用queryRows()別名
$rs = $user->queryRows($sql, $params);
```
在使用```queryAll()queryRows()```進行原生SQL操作時,需要特別注意:
+ 1、需要手動填寫完整的表名字,包括分表標識,并且需要通過任意表實例來運行
+ 2、盡量使用參數綁定,而不應直接使用參數來拼接SQL語句,慎防SQL注入攻擊
下面是不好的寫法,很有可能會導致SQL注入攻擊
```php
// 存在SQL注入的寫法
$id = 1;
$sql = "SELECT * FROM tbl_demo WHERE id = $id";
$rows = $this->getORM()->queryAll($sql);
```
對于外部不可信的輸入數據,應改用參數傳遞的方式。
```php
// 使用參數綁定方式
$id = 1;
$sql = "SELECT * FROM tbl_demo WHERE id = ?";
$rows = $this->getORM()->queryAll($sql, array($id));
```
查詢總數:
```php
// SELECT COUNT(id) FROM tbl_user
var_dump($user->sum('id'));
// 輸出
string(3) "3"
```
查詢最小值:
```php
// SELECT MIN(age) FROM tbl_user
var_dump($user->min('age'));
// 輸出
string(2) "18"
```
查詢最大值:
```php
// SELECT MAX(age) FROM tbl_user
var_dump($user->max('age'));
// 輸出
string(3) "100"
```
計算總和:
```php
// SELECT SUM(age) FROM tbl_user
var_dump($user->sum('age'));
// 輸出
string(3) "139"
```
#### CURD之刪除操作
操作|說明|示例|備注|是否PhalApi新增
---|---|---|---|---
delete()|刪除|```$user->where('id', 1)->delete();```|禁止無where條件的刪除操作|否
按條件進行刪除,并返回影響的行數:
```php
// DELETE FROM tbl_user WHERE (id = 404);
$user->where('id', 404)->delete();
```
請特別注意,PhalApi禁止全表刪除操作。即如果是全表刪除,將會被禁止,并拋出異常。如:
```php
// Exception: sorry, you can not delete the whole table
$user->delete();
```
## 事務操作、關聯查詢和其他操作
### 事務操作
以下是事務操作的一個示例。
```php
// Step 1: 開啟事務
\PhalApi\DI()->notorm->beginTransaction('db_demo');
// Step 2: 數據庫操作
\PhalApi\DI()->notorm->user->insert(array('name' => 'test1'));
\PhalApi\DI()->notorm->user->insert(array('name' => 'test2'));
// Step 3: 提交事務/回滾
\PhalApi\DI()->notorm->commit('db_demo');
//\PhalApi\DI()->notorm->rollback('db_demo');
```
### 關聯查詢
對于關聯查詢,簡單的關聯可使用NotORM封裝的方式,而復雜的關聯,如多個表的關聯查詢,則可以使用PhalApi封裝的接口。
如果是簡單的關聯查詢,可以使用NotORM支持的寫法,這樣的好處在于我們使用了一致的開發,并且能讓PhalApi框架保持分布式的操作方式。需要注意的是,關聯的表仍然需要在同一個數據庫。
以下是一個簡單的示例。假設我們有這樣的數據:
```sql
INSERT INTO `phalapi_user` VALUES ('1', 'wx_edebc', 'dogstar', '***', '4CHqOhe1', '1431790647', '');
INSERT INTO `phalapi_user_session_0` VALUES ('1', '1', 'ABC', '', '0', '0', '0', null);
```
那么對應關聯查詢的代碼如下面:
```php
// 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
$rs = \PhalApi\DI()->notorm->user_session_0
->select('expires_time, user.username, user.nickname')
->where('token', 'ABC')
->fetchRow();
var_dump($rs);
```
會得到類似這樣的輸出:
```php
array(3) {
["expires_time"]=>
string(1) "0"
["username"]=>
string(35) "wx_edebc"
["nickname"]=>
string(10) "dogstar"
}
```
這樣,我們就可以實現關聯查詢的操作。按照NotORM官網的說法,則是:
> 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')```這一行調用將會NotORM自動產生關聯操作,而ON的字段,則是這個字段關聯你配置的表結構,外鍵默認為:表名_id 。
如果是復雜的關聯查詢,則是建議使用原生的SQL語句,但仍然可以保持很好的寫法,如這樣一個示例:
```php
$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());
var_dump($rows);
```
如前面所述,這里需要手動填寫完整的表名,以及慎防SQL注入攻擊。
### 其他數據庫操作
有時,我們還需要進行一些其他的數據庫操作,如創建表、刪除表、添加表字段等。對于需要進行的數據庫操作,而上面所介紹的方法未能滿足時,可以使用更底層更通用的接口,即:```NotORM_Result::query($query, $parameters)```。
例如,刪除一張表。
```php
$user->query('DROP TABLE tbl_user', array());
```
## 數據庫分表
### 分表的配置
假設有以下多個數據庫表,它們的表結構一樣。
數據庫表|數據庫實例
---|---
tbl_demo|db_master
tbl_demo_0|db_master
tbl_demo_1|db_master
tbl_demo_2|db_master
為了使用分表存儲,可以修改數據庫表的配置,讓它支持分表的情況。
```php
return array(
'tables' => array(
'demo' => array(
'prefix' => 'tbl_',
'key' => 'id',
'map' => array(
array('db' => 'db_master'),
array('start' => 0, 'end' => 2, 'db' => 'db_master'),
),
),
),
);
```
上面配置map選項中```array('db' => 'master')```用于指定缺省主表使用master數據庫實例,而下一組映射關系則是用于配置連續在同一臺數據庫實例的分表區間,即tbl_demo_0、tbl_demo_1、tbl_demo_2都使用了master數據庫實例。
> 溫馨提示:當分表找不到時,PhalApi會自動退化使用缺省主表。
### Model子類實現分表邏輯
假設分別的規則是根據ID對3進行求余。當需要使用分表時,在使用Model基類的情況下,可以通過重寫```PhalApi\Model\NotORMModel::getTableName($id)```實現相應的分表規則。
```php
<?php
namespace App\Model;
use PhalApi\Model\NotORMModel as NotORM;
class Demo extends NotORM {
protected function getTableName($id) {
$tableName = 'demo';
if ($id !== null) {
$tableName .= '_' . ($id % 3);
}
return $tableName;
}
}
```
然后,便可使用之前一樣的CURD基本操作,但框架會自動匹配分表的映射。例如:
```php
$model = new App\Model\Demo();
$row = $model->get('3', 'id'); // 使用分表tbl_demo_0
$row = $model->get('10', 'id'); // 使用分表tbl_demo_1
$row = $model->get('2', 'id'); // 使用分表tbl_demo_2
```
回到使用Model基類的上下文,更進一步,我們可以通過```$this->getORM($id)```來獲取分表的實例從而進行分表的操作。如:
```php
<?php
namespace App\Model;
use PhalApi\Model\NotORMModel as NotORM;
class Demo extends NotORM {
public function getNameById($id) {
$row = $this->getORM($id)->select('name')->fetchRow();
return !empty($row) ? $row['name'] : '';
}
}
```
通過傳入不同的$id,即可獲取相應的分表實例。
### 自動生成SQL建表語句
把數據庫表的基本建表語句保存到./data目錄下,文件名與數據庫表名相同,后綴統一為“.sql”。如這里的./data/demo.sql文件。
```sql
`name` varchar(11) DEFAULT NULL,
```
需要注意的是,這里說的基本建表語句是指:僅是這個表所特有的字段,排除已固定公共有的自增主鍵id、擴展字段ext_data和CREATE TABLE關鍵字等。
然后可以使用phalapi-buildsqls腳本命令,快速自動生成demo缺省主表和全部分表的建表SQL語句。如下:
```bash
$ ./bin/phalapi-buildsqls ./config/dbs.php demo
```
正常情況下,會生成類似以下的SQL語句:
```sql
CREATE TABLE `demo` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(11) DEFAULT NULL,
`ext_data` text COMMENT 'json data here',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `tpl_demo_0` ... ...;
CREATE TABLE `tpl_demo_1` ... ...;
CREATE TABLE `tpl_demo_2` ... ...;
```
在將上面的SQL語句導入數據庫后,或者手動創建數據庫表后,便可以像之前那樣操作數據庫了。