在ThinkPHP中基礎的模型類就是`Think\Model`類,該類完成了基本的CURD、ActiveRecord模式、連貫操作和統計查詢,一些高級特性被封裝到另外的模型擴展中。
基礎模型類的設計非常靈活,甚至可以無需進行任何模型定義,就可以進行相關數據表的ORM和CURD操作,只有在需要封裝單獨的業務邏輯的時候,模型類才是必須被定義的。
# 定義與實例化(虛擬模型)
## 模型定義
> 模型類并非必須定義,只有當存在獨立的業務邏輯或者屬性的時候才需要定義。
模型類通常需要繼承系統的\Think\Model類或其子類,下面是一個Home\Model\UserModel類的定義:
~~~
namespace Home\Model;
use Think\Model;
class UserModel extends Model {
}
~~~
模型類的作用大多數情況是操作數據表的,如果按照系統的規范來命名模型類的話,大多數情況下是可以自動對應數據表。
模型類的命名規則是除去表前綴的數據表名稱,采用駝峰法命名,并且首字母大寫,然后加上模型層的名稱(默認定義是Model),例如:
| 模型名 | 約定對應數據表(假設數據庫的前綴定義是 think_) |
|-----|-----|
| UserModel | think_user |
| UserTypeModel | think_user_type |
如果你的規則和上面的系統約定不符合,那么需要設置Model類的數據表名稱屬性,以確保能夠找到對應的數據表。
### 數據表定義
在ThinkPHP的模型里面,有幾個關于數據表名稱的屬性定義:
| 屬性 | 說明 |
|-----|-----|
| tablePrefix | 定義模型對應數據表的前綴,如果未定義則獲取配置文件中的DB_PREFIX參數 |
| tableName | 不包含表前綴的數據表名稱,一般情況下默認和模型名稱相同,只有當你的表名和當前的模型類的名稱不同的時候才需要定義。 |
| trueTableName | 包含前綴的數據表名稱,也就是數據庫中的實際表名,該名稱無需設置,只有當上面的規則都不適用的情況或者特殊情況下才需要設置。 |
| dbName | 定義模型當前對應的數據庫名稱,只有當你當前的模型類對應的數據庫名稱和配置文件不同的時候才需要定義。 |
舉個例子來加深理解,例如,在數據庫里面有一個`think_categories`表,而我們定義的模型類名稱是`CategoryModel`,按照系統的約定,這個模型的名稱是Category,對應的數據表名稱應該是`think_category`(全部小寫),但是現在的數據表名稱是`think_categories`,因此我們就需要設置`tableName`屬性來改變默認的規則(假設我們已經在配置文件里面定義了`DB_PREFIX` 為 think_)。
~~~
namespace Home\Model;
use Think\Model;
class CategoryModel extends Model {
protected $tableName = 'categories';
}
~~~
注意這個屬性的定義不需要加表的前綴`think_`
如果我們需要CategoryModel模型對應操作的數據表是 `top_category`,那么我們只需要設置數據表前綴即可:
~~~
namespace Home\Model;
use Think\Model;
class CategoryModel extends Model {
protected $tablePrefix = 'top_';
}
~~~
如果你的數據表直接就是`category`,而沒有前綴,則可以設置`tablePrefix`為空字符串。
~~~
namespace Home\Model;
use Think\Model;
class CategoryModel extends Model {
protected $tablePrefix = '';
}
~~~
> 沒有表前綴的情況必須設置,否則會獲取當前配置文件中的 `DB_PREFIX`。
而對于另外一種特殊情況,我們需要操作的數據表是`top_categories`,這個時候我們就需要定義 `trueTableName` 屬性
~~~
namespace Home\Model;
use Think\Model;
class CategoryModel extends Model {
protected $trueTableName = 'top_categories';
}
~~~
> 注意`trueTableName`需要完整的表名定義。
除了數據表的定義外,還可以對數據庫進行定義(用于操作當前數據庫以外的數據表),例如 `top.top_categories`:
~~~
namespace Home\Model;
use Think\Model;
class CategoryModel extends Model {
protected $trueTableName = 'top_categories';
protected $dbName = 'top';
}
~~~
> 系統的規則下,tableName會轉換為小寫定義,但是trueTableName定義的數據表名稱是保持原樣。因此,如果你的數據表名稱需要區分大小寫的情況,那么可以通過設置trueTableName定義來解決。
## 實例化
在ThinkPHP中,可以無需進行任何模型定義。只有在需要封裝單獨的業務邏輯的時候,模型類才是必須被定義的,因此ThinkPHP在模型上有很多的靈活和方便性,讓你無需因為表太多而煩惱。
根據不同的模型定義,我們有幾種實例化模型的方法,根據需要采用不同的方式:
### 直接實例化
可以和實例化其他類庫一樣實例化模型類,例如:
~~~
$User = new \Home\Model\UserModel();
$Info = new \Admin\Model\InfoModel();
// 帶參數實例化
$New = new \Home\Model\NewModel('blog','think_',$connection);
~~~
模型類通常都是繼承系統的\Think\Model類,該類的架構方法有三個參數,分別是:
`Model(['模型名'],['數據表前綴'],['數據庫連接信息']);`
三個參數都是可選的,大多數情況下,我們根本無需傳入任何參數即可實例化。
| 參數 | 描述 |
|-----|-----|
| 模型名 | 模型的名稱 和數據表前綴一起配合用于自動識別數據表名稱 |
| 數據表前綴 | 當前數據表前綴 和模型名一起配合用于自動識別數據表名稱 |
| 數據庫連接信息 | 當前數據表的數據庫連接信息 如果沒有則獲取配置文件中的 |
> 數據表前綴傳入空字符串表示取當前配置的表前綴,如果當前數據表沒有前綴,則傳入null即可。
數據庫連接信息參數支持三種格式:
##### 1、字符串定義
字符串定義采用DSN格式定義,格式定義規范為:
`數據庫類型://用戶名:密碼@數據庫主機名或者IP:數據庫端口/數據庫名#字符集`
例如:
~~~
new \Home\Model\NewModel('blog','think_','mysql://root:1234@localhost/demo');
~~~
##### 2、數組定義
可以傳入數組格式的數據庫連接信息,例如:
~~~
$connection = array(
'db_type' => 'mysql',
'db_host' => '127.0.0.1',
'db_user' => 'root',
'db_pwd' => '12345',
'db_port' => 3306,
'db_name' => 'demo',
'db_charset' => 'utf8',
);
new \Home\Model\NewModel('new','think_',$connection);
~~~
如果需要的話,還可以傳入更多的連接參數,包括數據的部署模式和調試模式設定,例如:
~~~
$connection = array(
'db_type' => 'mysql',
'db_host' => '192.168.1.2,192.168.1.3',
'db_user' => 'root',
'db_pwd' => '12345',
'db_port' => 3306,
'db_name' => 'demo',
'db_charset' => 'utf8',
'db_deploy_type'=> 1,
'db_rw_separate'=> true,
'db_debug' => true,
);
// 分布式數據庫部署 并且采用讀寫分離 開啟數據庫調試模式
new \Home\Model\NewModel('new','think_',$connection);
~~~
> 注意,如果設置了db\_debug參數,那么數據庫調試模式就不再受APP\_DEBUG常量影響。
##### 3、配置定義
我們可以事先在配置文件中定義好數據庫連接信息,然后在實例化的時候直接傳入配置的名稱即可,例如:
~~~
//數據庫配置1
'DB_CONFIG1' => array(
'db_type' => 'mysql',
'db_user' => 'root',
'db_pwd' => '1234',
'db_host' => 'localhost',
'db_port' => '3306',
'db_name' => 'thinkphp'
),
//數據庫配置2
'DB_CONFIG2' => 'mysql://root:1234@localhost:3306/thinkphp',
~~~
在配置文件中定義數據庫連接信息的時候也支持字符串和數組格式,格式和上面實例化傳入的參數一樣。
然后,我們就可以這樣實例化模型類傳入連接信息:
~~~
new \Home\Model\NewModel('new','think_','DB_CONFIG1');
new \Home\Model\BlogModel('blog','think_','DB_CONFIG2');
~~~
事實上,當我們實例化的時候沒有傳入任何的數據庫連接信息的時候,系統其實默認會獲取配置文件中的相關配置參數,包括:
~~~
'DB_TYPE' => '', // 數據庫類型
'DB_HOST' => '', // 服務器地址
'DB_NAME' => '', // 數據庫名
'DB_USER' => '', // 用戶名
'DB_PWD' => '', // 密碼
'DB_PORT' => '', // 端口
'DB_PREFIX' => '', // 數據庫表前綴
'DB_DSN' => '', // 數據庫連接DSN 用于PDO方式
'DB_CHARSET' => 'utf8', // 數據庫的編碼 默認為utf8
~~~
如果應用配置文件中有配置上述數據庫連接信息的話,實例化模型將會變得非常簡單。
## D方法實例化
上面實例化的時候我們需要傳入完整的類名,系統提供了一個快捷方法D用于數據模型的實例化操作。
要實例化自定義模型類,可以使用下面的方式:
~~~
<?php
//實例化模型
$User = D('User');
// 相當于 $User = new \Home\Model\UserModel();
// 執行具體的數據操作
$User->select();
~~~
> 當 `\Home\Model\UserModel` 類不存在的時候,D函數會嘗試實例化公共模塊下面的 `\Common\Model\UserModel` 類。
D方法的參數就是模型的名稱,并且和模型類的大小寫定義是一致的,例如:
| 參數 | 實例化的模型文件(假設當前模塊為Home) |
|-----|-----|
| User | 對應的模型類文件的 \Home\Model\UserModel.class.php |
| UserType | 對應的模型類文件的 \Home\Model\UserTypeModel.class.php |
> 如果在Linux環境下面,一定要注意D方法實例化的時候的模型名稱的大小寫。
D方法可以自動檢測模型類,如果存在自定義的模型類,則實例化自定義模型類,如果不存在,則會實例化系統的\Think\Model基類,同時對于已實例化過的模型,不會重復實例化。
~~~
D方法還可以支持跨模塊調用,需要使用:
//實例化Admin模塊的User模型
D('Admin/User');
//實例化Extend擴展命名空間下的Info模型
D('Extend://Editor/Info');
~~~
> 注意:跨模塊實例化模型類的時候 不支持自動加載公共模塊的模型類。
### M方法實例化模型
D方法實例化模型類的時候通常是實例化某個具體的模型類,如果你僅僅是對數據表進行基本的CURD操作的話,使用M方法實例化的話,由于不需要加載具體的模型類,所以性能會更高。
例如:
~~~
// 使用M方法實例化
$User = M('User');
// 和用法 $User = new \Think\Model('User'); 等效
// 執行其他的數據操作
$User->select();
~~~
M方法也可以支持跨庫操作,例如:
~~~
// 使用M方法實例化 操作db_name數據庫的ot_user表
$User = M('db_name.User','ot_');
// 執行其他的數據操作
$User->select();
~~~
M方法的參數和\Think\Model類的參數是一樣的,也就是說,我們也可以這樣實例化:
~~~
$New = M('new','think_',$connection);
// 等效于 $New = new \Think\Model('new','think_',$connection);
~~~
具體的參數含義可以參考前面的介紹。
M方法實例化的時候,默認情況下是直接實例化系統的\Think\Model類,如果我們希望實例化其他的公共模型類的話,可以使用如下方法:
~~~
$User = M('\Home\Model\CommonModel:User','think_','db_config');
// 相當于 $User = new \Home\Model\CommonModel('User','think_','db_config');
~~~
> 如果你的模型類有自己的業務邏輯,M方法是無法支持的,就算是你已經定義了具體的模型類,M方法實例化的時候是會直接忽略。
### 實例化空模型類
如果你僅僅是使用原生SQL查詢的話,不需要使用額外的模型類,實例化一個空模型類即可進行操作了,例如:
~~~
//實例化空模型
$Model = new Model();
//或者使用M快捷方法是等效的
$Model = M();
//進行原生的SQL查詢
$Model->query('SELECT * FROM think_user WHERE status = 1');
~~~
> 實例化空模型類后還可以用table方法切換到具體的數據表進行操作
我們在實例化的過程中,經常使用D方法和M方法,這兩個方法的區別在于M方法實例化模型無需用戶為每個數據表定義模型類,如果D方法沒有找到定義的模型類,則會自動調用M方法。
# 連接數據庫
ThinkPHP內置了抽象數據庫訪問層,把不同的數據庫操作封裝起來,我們只需要使用公共的Db類進行操作,而無需針對不同的數據庫寫不同的代碼和底層實現,Db類會自動調用相應的數據庫驅動來處理。目前包含了Mysql、SqlServer、PgSQL、Sqlite、Oracle、Ibase、Mongo等數據庫的支持,并且采用PDO方式。
如果應用需要使用數據庫,必須配置數據庫連接信息,數據庫的配置文件有多種定義方式。
## 一、全局配置定義
常用的配置方式是在應用配置文件或者模塊配置文件中添加下面的配置參數:
~~~
//數據庫配置信息
'DB_TYPE' => 'mysql', // 數據庫類型
'DB_HOST' => '127.0.0.1', // 服務器地址
'DB_NAME' => 'thinkphp', // 數據庫名
'DB_USER' => 'root', // 用戶名
'DB_PWD' => '123456', // 密碼
'DB_PORT' => 3306, // 端口
'DB_PREFIX' => 'think_', // 數據庫表前綴
'DB_CHARSET'=> 'utf8', // 字符集
'DB_DEBUG' => TRUE, // 數據庫調試模式 開啟后可以記錄SQL日志
~~~
數據庫的類型由**DB_TYPE**參數設置。
配置文件定義的數據庫連接信息一般是系統默認采用的,因為一般一個應用的數據庫訪問配置是相同的。該方法系統在連接數據庫的時候會自動獲取,無需手動連接。
可以對每個模塊定義不同的數據庫連接信息,如果開啟了調試模式的話,還可以在不同的應用狀態的配置文件里面定義獨立的數據庫配置信息。
## 二、模型類定義
如果在某個模型類里面定義了`connection`屬性的話,則實例化該自定義模型的時候會采用定義的數據庫連接信息,而不是配置文件中設置的默認連接信息,通常用于某些數據表位于當前數據庫連接之外的其它數據庫,例如:
~~~
//在模型里單獨設置數據庫連接信息
namespace Home\Model;
use Think\Model;
class UserModel extends Model{
protected $connection = array(
'db_type' => 'mysql',
'db_user' => 'root',
'db_pwd' => '1234',
'db_host' => 'localhost',
'db_port' => '3306',
'db_name' => 'thinkphp',
'db_charset' => 'utf8',
);
}
~~~
也可以采用字符串方式定義,定義格式為:
##### 數據庫類型://用戶名:密碼@數據庫地址:數據庫端口/數據庫名#字符集
例如:
~~~
//在模型里單獨設置數據庫連接信息
namespace Home\Model;
use Think\Model;
class UserModel extends Model{
//或者使用字符串定義
protected $connection = 'mysql://root:1234@localhost:3306/thinkphp#utf8';
}
~~~
如果我們已經在配置文件中配置了額外的數據庫連接信息,例如:
~~~
//數據庫配置1
'DB_CONFIG1' => array(
'db_type' => 'mysql',
'db_user' => 'root',
'db_pwd' => '1234',
'db_host' => 'localhost',
'db_port' => '3306',
'db_name' => 'thinkphp',
'db_charset'=> 'utf8',
),
//數據庫配置2
'DB_CONFIG2' => 'mysql://root:1234@localhost:3306/thinkphp#utf8';
~~~
那么,我們可以把模型類的屬性定義改為:
~~~
//在模型里單獨設置數據庫連接信息
namespace Home\Model;
use Think\Model;
class UserModel extends Model{
//調用配置文件中的數據庫配置1
protected $connection = 'DB_CONFIG1';
}
~~~
~~~
//在模型里單獨設置數據庫連接信息
namespace Home\Model;
use Think\Model;
class InfoModel extends Model{
//調用配置文件中的數據庫配置1
protected $connection = 'DB_CONFIG2';
}
~~~
## 三、實例化定義
除了在模型定義的時候指定數據庫連接信息外,我們還可以在實例化的時候指定數據庫連接信息,例如: 如果采用的是M方法實例化模型的話,也可以支持傳入不同的數據庫連接信息,例如:
~~~
$User = M('User','other_','mysql://root:1234@localhost/demo#utf8');
~~~
表示實例化User模型,連接的是demo數據庫的other_user表,采用的連接信息是第三個參數配置的。如果我們在項目配置文件中已經配置了`DB_CONFIG2`的話,也可以采用:
~~~
$User = M('User','other_','DB_CONFIG2');
~~~
> 需要注意的是,ThinkPHP的數據庫連接是惰性的,所以并不是在實例化的時候就連接數據庫,而是在有實際的數據操作的時候才會去連接數據庫(額外的情況是,在系統第一次實例化模型的時候,會自動連接數據庫獲取相關模型類對應的數據表的字段信息)。
# 連貫操作
ThinkPHP模型基礎類提供的連貫操作方法(也有些框架稱之為鏈式操作),可以有效的提高數據存取的代碼清晰度和開發效率,并且支持所有的CURD操作。
使用也比較簡單, 假如我們現在要查詢一個User表的滿足狀態為1的前10條記錄,并希望按照用戶的創建時間排序 ,代碼如下:
~~~
$User->where('status=1')->order('create_time')->limit(10)->select();
~~~
這里的`where`、`order`和`limit`方法就被稱之為連貫操作方法,除了select方法必須放到最后一個外(因為select方法并不是連貫操作方法),連貫操作的方法調用順序沒有先后,例如,下面的代碼和上面的等效:
~~~
$User->order('create_time')->limit(10)->where('status=1')->select();
~~~
如果不習慣使用連貫操作的話,還支持直接使用參數進行查詢的方式。例如上面的代碼可以改寫為:
~~~
$User->select(array('order'=>'create_time','where'=>'status=1','limit'=>'10'));
~~~
使用數組參數方式的話,索引的名稱就是連貫操作的方法名稱。其實不僅僅是查詢方法可以使用連貫操作,包括所有的CURD方法都可以使用,例如:
~~~
$User->where('id=1')->field('id,name,email')->find();
$User->where('status=1 and id=1')->delete();
~~~
連貫操作通常只有一個參數,并且僅在當此查詢或者操作有效,完成后會自動清空連貫操作的所有傳值(有個別特殊的連貫操作有多個參數,并且會記錄當前的傳值)。簡而言之,連貫操作的結果不會帶入以后的查詢。
系統支持的連貫操作方法有:
| 連貫操作 | 作用 | 支持的參數類型 |
|-----|-----|-----|
| where* | 用于查詢或者更新條件的定義 | 字符串、數組和對象 |
| table | 用于定義要操作的數據表名稱 | 字符串和數組 |
| alias | 用于給當前數據表定義別名 | 字符串 |
| data | 用于新增或者更新數據之前的數據對象賦值 | 數組和對象 |
| field | 用于定義要查詢的字段(支持字段排除) | 字符串和數組 |
| order | 用于對結果排序 | 字符串和數組 |
| limit | 用于限制查詢結果數量 | 字符串和數字 |
| page | 用于查詢分頁(內部會轉換成limit) | 字符串和數字 |
| group | 用于對查詢的group支持 | 字符串 |
| having | 用于對查詢的having支持 | 字符串 |
| join* | 用于對查詢的join支持 | 字符串和數組 |
| union* | 用于對查詢的union支持 | 字符串、數組和對象 |
| distinct | 用于查詢的distinct支持 | 布爾值 |
| lock | 用于數據庫的鎖機制 | 布爾值 |
| cache | 用于查詢緩存 | 支持多個參數 |
| relation | 用于關聯查詢(需要關聯模型支持) | 字符串 |
| result | 用于返回數據轉換 | 字符串 |
| validate | 用于數據自動驗證 | 數組 |
| auto | 用于數據自動完成 | 數組 |
| filter | 用于數據過濾 | 字符串 |
| scope* | 用于命名范圍 | 字符串、數組 |
| bind* | 用于數據綁定操作 | 數組或多個參數 |
| token | 用于令牌驗證 | 布爾值 |
| comment | 用于SQL注釋 | 字符串 |
| index | 用于數據集的強制索引(3.2.3新增) | 字符串 |
| strict | 用于數據入庫的嚴格檢測(3.2.3新增) | 布爾值 |
> 所有的連貫操作都返回當前的模型實例對象(this),其中帶*標識的表示支持多次調用。
具體的連貫操作方法是什么用途和可用參數,參見TP手冊里[連貫操作]子章節(http://document.thinkphp.cn/manual_3_2.html#continuous_operation),我就不贅述了。
# CURD 與自動驗證和自動完成
ThinkPHP提供了靈活和方便的數據操作方法,對數據庫操作的四個基本操作(CURD):創建、更新、讀取和刪除的實現是最基本的,也是必須掌握的,在這基礎之上才能熟悉更多實用的數據操作方法。
CURD操作通常是可以和連貫操作配合完成的。
## 數據創建
在進行數據操作之前,我們往往需要手動創建需要的數據,例如對于提交的表單數據:
~~~
// 獲取表單的POST數據
$data['name'] = $_POST['name'];
$data['email'] = $_POST['email'];
// 更多的表單數據值獲取
//……
~~~
## 創建數據對象
ThinkPHP可以幫助你快速地創建數據對象,最典型的應用就是自動根據表單數據創建數據對象,這個優勢在一個數據表的字段非常之多的情況下尤其明顯。
很簡單的例子:
~~~
// 實例化User模型
$User = M('User');
// 根據表單提交的POST數據創建數據對象
$User->create();
~~~
Create方法支持從其它方式創建數據對象,例如,從其它的數據對象,或者數組等
~~~
$data['name'] = 'ThinkPHP';
$data['email'] = 'ThinkPHP@gmail.com';
$User->create($data);
~~~
甚至還可以支持從對象創建新的數據對象
~~~
// 從User數據對象創建新的Member數據對象
$User = stdClass();
$User->name = 'ThinkPHP';
$User->email = 'ThinkPHP@gmail.com';
$Member = M("Member");
$Member->create($User);
~~~
創建完成的數據可以直接讀取和修改,例如:
~~~
$data['name'] = 'ThinkPHP';
$data['email'] = 'ThinkPHP@gmail.com';
$User->create($data);
// 創建完成數據對象后可以直接讀取數據
echo $User->name;
echo $User->email;
// 也可以直接修改創建完成的數據
$User->name = 'onethink'; // 修改name字段數據
$User->status = 1; // 增加新的字段數據
~~~
## 數據操作狀態
create方法的第二個參數可以指定創建數據的操作狀態,默認情況下是自動判斷是寫入還是更新操作。
也可以顯式指定操作狀態,例如:
~~~
$Member = M("User");
// 指定更新數據操作狀態
$Member->create($_POST,Model::MODEL_UPDATE);
~~~
系統內置的數據操作包括`Model::MODEL_INSERT`(或者1)和`Model::MODEL_UPDATE`(或者2),當沒有指定的時候,系統根據數據源是否包含主鍵數據來自動判斷,如果存在主鍵數據,就當成`Model::MODEL_UPDATE`操作。
不同的數據操作狀態可以定義不同的數據驗證和自動完成機制,所以,你可以自定義自己需要的數據操作狀態,例如,可以設置登錄操作的數據狀態(假設為3):
~~~
$Member = M("User");
// 指定更新數據操作狀態
$Member->create($_POST,3);
~~~
事實上,create方法所做的工作遠非這么簡單,在創建數據對象的同時,完成了一系列的工作,我們來看下create方法的工作流程就能明白:
| 步驟 | 說明 | 返回 |
|-----|-----|-----|
| 1 | 獲取數據源(默認是POST數組) | |
| 2 | 驗證數據源合法性(非數組或者對象會過濾) | 失敗則返回false |
| 3 | 檢查字段映射 | |
| 4 | 判斷數據狀態(新增或者編輯,指定或者自動判斷) | |
| 5 | 數據自動驗證 | 失敗則返回false |
| 6 | 表單令牌驗證 | 失敗則返回false |
| 7 | 表單數據賦值(過濾非法字段和字符串處理) | |
| 8 | 數據自動完成 | |
| 9 | 生成數據對象(保存在內存) | |
因此,我們熟悉的令牌驗證、[自動驗證](/thinkphp/thinkphp/1776)和[自動完成](/thinkphp/thinkphp/1777)功能,其實都必須通過create方法才能生效。
如果沒有定義自動驗證的話,create方法的返回值是創建完成的數據對象數組,例如:
~~~
$data['name'] = 'thinkphp';
$data['email'] = 'thinkphp@gmail.com';
$data['status'] = 1;
$User = M('User');
$data = $User->create($data);
dump($data);
~~~
輸出結果為:
~~~
array (size=3)
'name' => string 'thinkphp' (length=8)
'email' => string 'thinkphp@gmail.com' (length=18)
'status'=> int 1
~~~
Create方法創建的數據對象是保存在內存中,并沒有實際寫入到數據庫中,直到使用`add`或者`save`方法才會真正寫入數據庫。
因此在沒有調用add或者save方法之前,我們都可以改變create方法創建的數據對象,例如:
~~~
$User = M('User');
$User->create(); //創建User數據對象
$User->status = 1; // 設置默認的用戶狀態
$User->create_time = time(); // 設置用戶的創建時間
$User->add(); // 把用戶對象寫入數據庫
~~~
如果只是想簡單創建一個數據對象,并不需要完成一些額外的功能的話,可以使用data方法簡單的創建數據對象。 使用如下:
~~~
// 實例化User模型
$User = M('User');
// 創建數據后寫入到數據庫
$data['name'] = 'ThinkPHP';
$data['email'] = 'ThinkPHP@gmail.com';
$User->data($data)->add();
~~~
Data方法也支持傳入數組和對象,使用data方法創建的數據對象不會進行自動驗證和過濾操作,請自行處理。但在進行add或者save操作的時候,數據表中不存在的字段以及非法的數據類型(例如對象、數組等非標量數據)是會自動過濾的,不用擔心非數據表字段的寫入導致SQL錯誤的問題。
### 支持的連貫操作
在執行create方法之前,我們可以調用相關的連貫操作方法,配合完成數據創建操作。
create方法支持的連貫操作方法包括:
| 連貫操作 | 作用 | 支持的參數類型 |
|-----|-----|-----|
| field | 用于定義合法的字段 | 字符串和數組 |
| validate | 用于數據自動驗證 | 數組 |
| auto | 用于數據自動完成 | 數組 |
| token | 用于令牌驗證 | 布爾值 |
更多的用法參考后續的內容。
### 字段合法性過濾
如果在create方法之前調用field方法,則表示只允許創建指定的字段數據,其他非法字段將會被過濾,例如:
~~~
$data['name'] = 'thinkphp';
$data['email'] = 'thinkphp@gmail.com';
$data['status'] = 1;
$data['test'] = 'test';
$User = M('User');
$data = $User->field('name,email')->create($data);
dump($data);
~~~
輸出結果為:
~~~
array (size=2)
'name' => string 'thinkphp' (length=8)
'email' => string 'thinkphp@gmail.com' (length=18)
~~~
最終只有`name`和`email`字段的數據被允許寫入,`status`和`test`字段直接被過濾了,哪怕status也是數據表中的合法字段。
如果我們有自定義模型類,對于數據新增和編輯操作的話,我們還可以直接在模型類里面通過設置`insertFields`和`updateFields`屬性來定義允許的字段,例如:
~~~
namespace Home\Model;
use Think\Model;
class UserModel extends Model{
protected $insertFields = 'name,email'; // 新增數據的時候允許寫入name和email字段
protected $updateFields = 'email'; // 編輯數據的時候只允許寫入email字段
}
~~~
## 數據新增
ThinkPHP的數據寫入操作使用**add方法**,使用示例如下:
~~~
$User = M("User"); // 實例化User對象
$data['name'] = 'ThinkPHP';
$data['email'] = 'ThinkPHP@gmail.com';
$User->add($data);
~~~
如果是Mysql數據庫的話,還可以支持在數據插入時允許更新操作:
~~~
add($data='',$options=array(),$replace=false)
~~~
其中add方法增加$replace參數(是否添加數據時允許覆蓋),true表示覆蓋,默認為false
或者使用data方法連貫操作
~~~
$User = M("User"); // 實例化User對象
$User->data($data)->add();
~~~
如果在add之前已經創建數據對象的話(例如使用了create或者data方法),add方法就不需要再傳入數據了。 使用create方法的例子:
~~~
$User = M("User"); // 實例化User對象
// 根據表單提交的POST數據創建數據對象
if($User->create()){
$result = $User->add(); // 寫入數據到數據庫
if($result){
// 如果主鍵是自動增長型 成功后返回值就是最新插入的值
$insertId = $result;
}
}
~~~
> create方法并不算是連貫操作,因為其返回值可能是布爾值,所以必須要進行嚴格判斷。
### 支持的連貫操作
在執行add方法之前,我們可以調用相關的連貫操作方法,配合完成數據寫入操作。
寫入操作支持的連貫操作方法包括:
| 連貫操作 | 作用 | 支持的參數類型 |
|-----|-----|-----|
| table | 用于定義要操作的數據表名稱 | 字符串和數組 |
| data | 用于指定要寫入的數據對象 | 數組和對象 |
| field | 用于定義要寫入的字段 | 字符串和數組 |
| relation | 用于關聯查詢(需要關聯模型支持) | 字符串 |
| validate | 用于數據自動驗證 | 數組 |
| auto | 用于數據自動完成 | 數組 |
| filter | 用于數據過濾 | 字符串 |
| scope | 用于命名范圍 | 字符串、數組 |
| bind | 用于數據綁定操作 | 數組 |
| token | 用于令牌驗證 | 布爾值 |
| comment | 用于SQL注釋 | 字符串 |
| fetchSql | 不執行SQL而只是返回SQL | 布爾值 |
可以支持不執行SQL而只是返回SQL語句,例如:
~~~
$User = M("User"); // 實例化User對象
$data['name'] = 'ThinkPHP';
$data['email'] = 'ThinkPHP@gmail.com';
$sql = $User->fetchSql(true)->add($data);
echo $sql;
// 輸出結果類似于
// INSERT INTO think_user (name,email) VALUES ('ThinkPHP','ThinkPHP@gmail.com')
~~~
##### 字段過濾
如果寫入了數據表中不存在的字段數據,則會被直接過濾,例如:
~~~
$data['name'] = 'thinkphp';
$data['email'] = 'thinkphp@gmail.com';
$data['test'] = 'test';
$User = M('User');
$User->data($data)->add();
~~~
其中test字段是不存在的,所以寫入數據的時候會自動過濾掉。
> 在3.2.2版本以上,如果開啟調試模式的話,則會拋出異常,提示:`非法數據對象:[test=>test]`
如果在add方法之前調用field方法,則表示只允許寫入指定的字段數據,其他非法字段將會被過濾,例如:
~~~
$data['name'] = 'thinkphp';
$data['email'] = 'thinkphp@gmail.com';
$data['test'] = 'test';
$User = M('User');
$User->field('name')->data($data)->add();
~~~
最終只有name字段的數據被允許寫入,email和test字段直接被過濾了,哪怕email也是數據表中的合法字段。
##### 字段內容過濾
通過filter方法可以對數據的值進行過濾處理,例如:
~~~
$data['name'] = '<b>thinkphp</b>';
$data['email'] = 'thinkphp@gmail.com';
$User = M('User');
$User->data($data)->filter('strip_tags')->add();
~~~
寫入數據庫的時候會把name字段的值轉化為`thinkphp`。
> filter方法的參數是一個回調類型,支持函數或者閉包定義。
### 批量寫入
在某些情況下可以支持數據的批量寫入,例如:
~~~
// 批量添加數據
$dataList[] = array('name'=>'thinkphp','email'=>'thinkphp@gamil.com');
$dataList[] = array('name'=>'onethink','email'=>'onethink@gamil.com');
$User->addAll($dataList);
~~~
> **該功能需要3.2.3以上版本,3.2.3以下版本僅對mysql數據庫支持**
### 數據讀取
在ThinkPHP中讀取數據的方式很多,通常分為讀取數據、讀取數據集和讀取字段值。
數據查詢方法支持的連貫操作方法有:
| 連貫操作 | 作用 | 支持的參數類型 |
|-----|-----|-----|
| where | 用于查詢或者更新條件的定義 | 字符串、數組和對象 |
| table | 用于定義要操作的數據表名稱 | 字符串和數組 |
| alias | 用于給當前數據表定義別名 | 字符串 |
| field | 用于定義要查詢的字段(支持字段排除) | 字符串和數組 |
| order | 用于對結果排序 | 字符串和數組 |
| group | 用于對查詢的group支持 | 字符串 |
| having | 用于對查詢的having支持 | 字符串 |
| join | 用于對查詢的join支持 | 字符串和數組 |
| union | 用于對查詢的union支持 | 字符串、數組和對象 |
| distinct | 用于查詢的distinct支持 | 布爾值 |
| lock | 用于數據庫的鎖機制 | 布爾值 |
| cache | 用于查詢緩存 | 支持多個參數 |
| relation | 用于關聯查詢(需要關聯模型支持) | 字符串 |
| result | 用于返回數據轉換 | 字符串 |
| scope | 用于命名范圍 | 字符串、數組 |
| bind | 用于數據綁定操作 | 數組 |
| comment | 用于SQL注釋 | 字符串 |
| fetchSql | 不執行SQL而只是返回SQL | 布爾值 |
> 注意:某些情況下有些連貫操作是無效的,例如limit方法對find方法是無效的。
### 讀取數據
讀取數據是指讀取數據表中的一行數據(或者關聯數據),主要通過`find`方法完成,例如:
~~~
$User = M("User"); // 實例化User對象
// 查找status值為1name值為think的用戶數據
$data = $User->where('status=1 AND name="thinkphp"')->find();
dump($data);
~~~
find方法查詢數據的時候可以配合相關的連貫操作方法,其中最關鍵的則是where方法,如何使用where方法我們會在[查詢語言](#)章節中詳細描述。
如果查詢出錯,find方法返回false,如果查詢結果為空返回NULL,查詢成功則返回一個關聯數組(鍵值是字段名或者別名)。 如果上面的查詢成功的話,會輸出:
~~~
array (size=3)
'name' => string 'thinkphp' (length=8)
'email' => string 'thinkphp@gmail.com' (length=18)
'status'=> int 1
~~~
> 即使滿足條件的數據不止一個,find方法也只會返回第一條記錄(可以通過order方法排序后查詢)。
還可以用data方法獲取查詢后的數據對象(查詢成功后)
~~~
$User = M("User"); // 實例化User對象
// 查找status值為1name值為think的用戶數據
$User->where('status=1 AND name="thinkphp"')->find();
dump($User->data());
~~~
### 讀取數據集
讀取數據集其實就是獲取數據表中的多行記錄(以及關聯數據),使用`select`方法,使用示例:
~~~
$User = M("User"); // 實例化User對象
// 查找status值為1的用戶數據 以創建時間排序 返回10條數據
$list = $User->where('status=1')->order('create_time')->limit(10)->select();
~~~
如果查詢出錯,select的返回值是false,如果查詢結果為空,則返回NULL,否則返回二維數組。
### 讀取字段值
讀取字段值其實就是獲取數據表中的某個列的多個或者單個數據,最常用的方法是 `getField`方法。
示例如下:
~~~
$User = M("User"); // 實例化User對象
// 獲取ID為3的用戶的昵稱
$nickname = $User->where('id=3')->getField('nickname');
~~~
默認情況下,當只有一個字段的時候,返回滿足條件的數據表中的該字段的第一行的值。
如果需要返回整個列的數據,可以用:
~~~
$User->getField('id',true); // 獲取id數組
//返回數據格式如array(1,2,3,4,5)一維數組,其中value就是id列的每行的值
~~~
如果傳入多個字段的話,默認返回一個關聯數組:
~~~
$User = M("User"); // 實例化User對象
// 獲取所有用戶的ID和昵稱列表
$list = $User->getField('id,nickname');
//兩個字段的情況下返回的是array(`id`=>`nickname`)的關聯數組,以id的值為key,nickname字段值為value
~~~
這樣返回的list是一個數組,鍵名是用戶的id字段的值,鍵值是用戶的昵稱nickname。
如果傳入多個字段的名稱,例如:
~~~
$list = $User->getField('id,nickname,email');
//返回的數組格式是array(`id`=>array(`id`=>value,`nickname`=>value,`email`=>value))是一個二維數組,key還是id字段的值,但value是整行的array數組,類似于select()方法的結果遍歷將id的值設為數組key
~~~
返回的是一個二維數組,類似select方法的返回結果,區別的是這個二維數組的鍵名是用戶的id(準確的說是getField方法的第一個字段名)。
如果我們傳入一個字符串分隔符:
~~~
$list = $User->getField('id,nickname,email',':');
~~~
那么返回的結果就是一個數組,鍵名是用戶id,鍵值是 `nickname:email`的輸出字符串。
getField方法還可以支持限制數量,例如:
~~~
$this->getField('id,name',5); // 限制返回5條記錄
$this->getField('id',3); // 獲取id數組 限制3條記錄
~~~
可以配合使用order方法使用。更多的查詢方法可以參考[查詢語言](#)章節。
## 數據更新
ThinkPHP的數據更新操作包括更新數據和更新字段方法。
### 更新數據
更新數據使用`save`方法,例如:
~~~
$User = M("User"); // 實例化User對象
// 要修改的數據對象屬性賦值
$data['name'] = 'ThinkPHP';
$data['email'] = 'ThinkPHP@gmail.com';
$User->where('id=5')->save($data); // 根據條件更新記錄
~~~
也可以改成對象方式來操作:
~~~
$User = M("User"); // 實例化User對象
// 要修改的數據對象屬性賦值
$User->name = 'ThinkPHP';
$User->email = 'ThinkPHP@gmail.com';
$User->where('id=5')->save(); // 根據條件更新記錄
~~~
數據對象賦值的方式,save方法無需傳入數據,會自動識別。
> 注意:save方法的返回值是**影響的記錄數**,如果返回false則表示更新出錯,因此一定要用恒等來判斷是否更新失敗。
為了保證數據庫的安全,避免出錯更新整個數據表,如果沒有任何更新條件,數據對象本身也不包含主鍵字段的話,save方法不會更新任何數據庫的記錄。
因此下面的代碼不會更改數據庫的任何記錄
~~~
$User->save($data);
~~~
除非使用下面的方式:
~~~
$User = M("User"); // 實例化User對象
// 要修改的數據對象屬性賦值
$data['id'] = 5;
$data['name'] = 'ThinkPHP';
$data['email'] = 'ThinkPHP@gmail.com';
$User->save($data); // 根據條件保存修改的數據
~~~
如果id是數據表的主鍵的話,系統自動會把主鍵的值作為更新條件來更新其他字段的值。
數據更新方法支持的連貫操作方法有:
| 連貫操作 | 作用 | 支持的參數類型 |
|-----|-----|-----|
| where | 用于查詢或者更新條件的定義 | 字符串、數組和對象 |
| table | 用于定義要操作的數據表名稱 | 字符串和數組 |
| alias | 用于給當前數據表定義別名 | 字符串 |
| field | 用于定義允許更新的字段 | 字符串和數組 |
| order | 用于對數據排序 | 字符串和數組 |
| lock | 用于數據庫的鎖機制 | 布爾值 |
| relation | 用于關聯更新(需要關聯模型支持) | 字符串 |
| scope | 用于命名范圍 | 字符串、數組 |
| bind | 用于數據綁定操作 | 數組 |
| comment | 用于SQL注釋 | 字符串 |
| fetchSql | 不執行SQL而只是返回SQL | 布爾值 |
##### 字段和數據過濾
和add方法一樣,save方法支持使用`field`方法過濾字段和`filter`方法過濾數據,例如:
~~~
$User = M("User"); // 實例化User對象
// 要修改的數據對象屬性賦值
$data['name'] = 'test';
$data['email'] = '<b>test@gmail.com</b>';
$User->where('id=5')->field('email')->filter('strip_tags')->save($data); // 根據條件保存修改的數據
~~~
當使用field('email')的時候,只允許更新email字段的值(采用strip_tags方法過濾),name字段的值將不會被修改。
還有一種方法是通過create或者data方法創建要更新的數據對象,然后進行保存操作,這樣save方法的參數可以不需要傳入。
~~~
$User = M("User"); // 實例化User對象
// 要修改的數據對象屬性賦值
$data['name'] = 'ThinkPHP';
$data['email'] = 'ThinkPHP@gmail.com';
$User->where('id=5')->data($data)->save(); // 根據條件保存修改的數據
~~~
使用create方法的例子:
~~~
$User = M("User"); // 實例化User對象
// 根據表單提交的POST數據創建數據對象
$User->create();
$User->save(); // 根據條件保存修改的數據
~~~
### 更新字段
如果只是更新個別字段的值,可以使用`setField`方法。
使用示例:
~~~
$User = M("User"); // 實例化User對象
// 更改用戶的name值
$User-> where('id=5')->setField('name','ThinkPHP');
~~~
setField方法支持同時更新多個字段,只需要傳入數組即可,例如:
~~~
$User = M("User"); // 實例化User對象
// 更改用戶的name和email的值
$data = array('name'=>'ThinkPHP','email'=>'ThinkPHP@gmail.com');
$User-> where('id=5')->setField($data);
~~~
而對于統計字段(通常指的是數字類型)的更新,系統還提供了`setInc`和`setDec`方法。
~~~
$User = M("User"); // 實例化User對象
$User->where('id=5')->setInc('score',3); // 用戶的積分加3
$User->where('id=5')->setInc('score'); // 用戶的積分加1
$User->where('id=5')->setDec('score',5); // 用戶的積分減5
$User->where('id=5')->setDec('score'); // 用戶的積分減1
~~~
3.2.3版本開始,setInc和setDec方法支持延遲更新,用法如下:
~~~
$Article = M("Article"); // 實例化Article對象
$Article->where('id=5')->setInc('view',1); // 文章閱讀數加1
$Article->where('id=5')->setInc('view',1,60); // 文章閱讀數加1,并且延遲60秒更新(寫入)
~~~
## 數據刪除
ThinkPHP刪除數據使用delete方法,例如:
~~~
$Form = M('Form');
$Form->delete(5);
~~~
表示刪除主鍵為5的數據,delete方法可以刪除單個數據,也可以刪除多個數據,這取決于刪除條件,例如:
~~~
$User = M("User"); // 實例化User對象
$User->where('id=5')->delete(); // 刪除id為5的用戶數據
$User->delete('1,2,5'); // 刪除主鍵為1,2和5的用戶數據
$User->where('status=0')->delete(); // 刪除所有狀態為0的用戶數據
~~~
delete方法的返回值是刪除的記錄數,如果返回值是false則表示SQL出錯,返回值如果為0表示沒有刪除任何數據。
也可以用order和limit方法來限制要刪除的個數,例如:
~~~
// 刪除所有狀態為0的5 個用戶數據 按照創建時間排序
$User->where('status=0')->order('create_time')->limit('5')->delete();
~~~
為了避免錯刪數據,如果沒有傳入任何條件進行刪除操作的話,不會執行刪除操作,例如:
~~~
$User = M("User"); // 實例化User對象
$User->delete();
~~~
不會刪除任何數據,如果你確實要刪除所有的記錄,除非使用下面的方式:
~~~
$User = M("User"); // 實例化User對象
$User->where('1')->delete();
~~~
數據刪除方法支持的連貫操作方法有:
| 連貫操作 | 作用 | 支持的參數類型 |
|-----|-----|-----|
| where | 用于查詢或者更新條件的定義 | 字符串、數組和對象 |
| table | 用于定義要操作的數據表名稱 | 字符串和數組 |
| alias | 用于給當前數據表定義別名 | 字符串 |
| order | 用于對數據排序 | 字符串和數組 |
| lock | 用于數據庫的鎖機制 | 布爾值 |
| relation | 用于關聯刪除(需要關聯模型支持) | 字符串 |
| scope | 用于命名范圍 | 字符串、數組 |
| bind | 用于數據綁定操作 | 數組 |
| comment | 用于SQL注釋 | 字符串 |
| fetchSql | 不執行SQL而只是返回SQL | 布爾值 |
# 幾種高級模型
## 視圖模型
### 視圖定義
視圖通常是指數據庫的視圖,視圖是一個虛擬表,其內容由查詢定義。同真實的表一樣,視圖包含一系列帶有名稱的列和行數據。但是,視圖并不在數據庫中以存儲的數據值集形式存在。行和列數據來自由定義視圖的查詢所引用的表,并且在引用視圖時動態生成。對其中所引用的基礎表來說,視圖的作用類似于篩選。定義視圖的篩選可以來自當前或其它數據庫的一個或多個表,或者其它視圖。分布式查詢也可用于定義使用多個異類源數據的視圖。如果有幾臺不同的服務器分別存儲組織中不同地區的數據,而您需要將這些服務器上相似結構的數據組合起來,這種方式就很有用。 視圖在有些數據庫下面并不被支持,但是ThinkPHP模擬實現了數據庫的視圖,該功能可以用于多表聯合查詢。非常適合解決HAS_ONE 和 BELONGS_TO 類型的關聯查詢。
要定義視圖模型,只需要繼承Think\Model\ViewModel,然后設置viewFields屬性即可。
例如下面的例子,我們定義了一個BlogView模型對象,其中包括了Blog模型的id、name、title和User模型的name,以及Category模型的title字段,我們通過創建BlogView模型來快速讀取一個包含了User名稱和類別名稱的Blog記錄(集)。
~~~
namespace Home\Model;
use Think\Model\ViewModel;
class BlogViewModel extends ViewModel {
public $viewFields = array(
'Blog'=>array('id','name','title'),
'Category'=>array('title'=>'category_name', '_on'=>'Blog.category_id=Category.id'),
'User'=>array('name'=>'username', '_on'=>'Blog.user_id=User.id'),
);
}
~~~
我們來解釋一下定義的格式代表了什么。
`$viewFields` 屬性表示視圖模型包含的字段,每個元素定義了某個數據表或者模型的字段。
例如:
~~~
'Blog'=>array('id','name','title');
~~~
表示BlogView視圖模型要包含Blog模型中的id、name和title字段屬性,這個其實很容易理解,就和數據庫的視圖要包含某個數據表的字段一樣。而Blog相當于是給Blog模型對應的數據表定義了一個別名。
默認情況下會根據定義的名稱自動獲取表名,如果希望指定數據表,可以使用:
~~~
'_table'=>"test_user"
// 3.2.2版本以上還可以使用簡化定義(自動獲取表前綴)
'_table'=>"__USER__"
~~~
如果希望給當前數據表定義另外的別名,可以使用
~~~
'_as'=>'myBlog'
~~~
BlogView視圖模式除了包含Blog模型之外,還包含了Category和User模型,下面的定義:
~~~
'Category'=>array('title'=>'category_name');
~~~
和上面類似,表示BlogView視圖模型還要包含Category模型的title字段,因為視圖模型里面已經存在了一個title字段,所以我們通過
~~~
'title'=>'category_name'
~~~
把Category模型的title字段映射為`category_name`字段,如果有多個字段,可以使用同樣的方式添加。
可以通過_on來給視圖模型定義關聯查詢條件,例如:
~~~
'_on'=>'Blog.category_id=Category.id'
~~~
理解之后,User模型的定義方式同樣也就很容易理解了。
~~~
Blog.categoryId=Category.id AND Blog.userId=User.id
~~~
最后,我們把視圖模型的定義翻譯成SQL語句就更加容易理解視圖模型的原理了。假設我們不帶任何其他條件查詢全部的字段,那么查詢的SQL語句就是
~~~
Select
Blog.id as id,
Blog.name as name,
Blog.title as title,
Category.title as category_name,
User.name as username
from think_blog Blog JOIN think_category Category JOIN think_user User
where Blog.category_id=Category.id AND Blog.user_id=User.id
~~~
視圖模型的定義并不需要先單獨定義其中的模型類,系統會默認按照系統的規則進行數據表的定位。如果Blog模型并沒有定義,那么系統會自動根據當前模型的表前綴和后綴來自動獲取對應的數據表。也就是說,如果我們并沒有定義Blog模型類,那么上面的定義后,系統在進行視圖模型的操作的時候會根據Blog這個名稱和當前的表前綴設置(假設為Think_ )獲取到對應的數據表可能是think_blog。
ThinkPHP還可以支持視圖模型的JOIN類型定義,我們可以把上面的視圖定義改成:
~~~
public $viewFields = array(
'Blog'=>array('id','name','title','_type'=>'LEFT'),
'Category'=>array('title'=>'category_name','_on'=>'Category.id=Blog.category_id','_type'=>'RIGHT'),
'User'=>array('name'=>'username','_on'=>'User.id=Blog.user_id'),
);
~~~
需要注意的是,這里的_type定義對下一個表有效,因此要注意視圖模型的定義順序。Blog模型的
~~~
'_type'=>'LEFT'
~~~
針對的是下一個模型Category而言,通過上面的定義,我們在查詢的時候最終生成的SQL語句就變成:
~~~
Select
Blog.id as id,
Blog.name as name,
Blog.title as title,
Category.title as category_name,
User.name as username
from think_blog Blog LEFT JOIN think_category Category ON Blog.category_id=Category.id RIGHT JOIN think_user User ON Blog.user_id=User.id
~~~
我們可以在試圖模型里面定義特殊的字段,例如下面的例子定義了一個統計字段
~~~
'Category'=>array('title'=>'category_name','COUNT(Blog.id)'=>'count','_on'=>'Category.id=Blog.category_id'),
~~~
### 視圖查詢
接下來,我們就可以和使用普通模型一樣對視圖模型進行操作了 。
~~~
$Model = D("BlogView");
$Model->field('id,name,title,category_name,username')->where('id>10')->order('id desc')->select();
~~~
看起來和普通的模型操作并沒有什么大的區別,可以和使用普通模型一樣進行查詢。如果發現查詢的結果存在重復數據,還可以使用group方法來處理。
~~~
$Model->field('id,name,title,category_name,username')->order('id desc')->group('id')->select();
~~~
我們可以看到,即使不定義視圖模型,其實我們也可以通過方法來操作,但是顯然非常繁瑣。
~~~
$Model = D("Blog");
$Model->table('think_blog Blog,think_category Category,think_user User')
->field('Blog.id,Blog.name,Blog.title,Category.title as category_name,User.name as username')
->order('Blog.id desc')
->where('Blog.category_id=Category.id AND Blog.user_id=User.id')
->select();
~~~
而定義了視圖模型之后,所有的字段會進行自動處理,添加表別名和字段別名,從而簡化了原來視圖的復雜查詢。如果不使用視圖模型,也可以用連貫操作的JOIN方法實現相同的功能。
## 關聯模型
### 關聯關系
通常我們所說的關聯關系包括下面三種:
~~~
一對一關聯 :ONE_TO_ONE,包括HAS_ONE 和 BELONGS_TO
一對多關聯 :ONE_TO_MANY,包括HAS_MANY 和 BELONGS_TO
多對多關聯 :MANY_TO_MANY
~~~
關聯關系必然有一個參照表,例如:
- 有一個員工檔案管理系統項目,這個項目要包括下面的一些數據表:基本信息表、員工檔案表、部門表、項目組表、銀行卡表(用來記錄員工的銀行卡資料)。
- 這些數據表之間存在一定的關聯關系,我們以員工基本信息表為參照來分析和其他表之間的關聯:
- 每個員工必然有對應的員工檔案資料,所以屬于HAS_ONE關聯;
- 每個員工必須屬于某個部門,所以屬于BELONGS_TO關聯;
- 每個員工可以有多個銀行卡,但是每張銀行卡只可能屬于一個員工,因此屬于HAS_MANY關聯;
- 每個員工可以同時在多個項目組,每個項目組同時有多個員工,因此屬于MANY_TO_MANY關聯;
- 分析清楚數據表之前的關聯關系后,我們才可以進行關聯定義和關聯操作。
### 關聯定義
ThinkPHP可以很輕松的完成數據表的關聯CURD操作,目前支持的關聯關系包括下面四種:
**HAS_ONE**、**BELONGS_TO**、**HAS_MANY**和**MANY_TO_MANY**。
一個模型根據業務模型的復雜程度可以同時定義多個關聯,不受限制,所有的關聯定義都統一在模型類的 $_link 成員變量里面定義,并且可以支持動態定義。要支持關聯操作,模型類必須繼承`Think\Model\RelationModel`類,關聯定義的格式是:
~~~
namespace Home\Model;
use Think\Model\RelationModel;
class UserModel extends RelationModel{
protected $_link = array(
'關聯1' => array(
'關聯屬性1' => '定義',
'關聯屬性N' => '定義',
),
'關聯2' => array(
'關聯屬性1' => '定義',
'關聯屬性N' => '定義',
),
'關聯3' => HAS_ONE, // 快捷定義
...
);
}
~~~
下面我們首先來分析下各個關聯方式的定義:
#### HAS_ONE
HAS_ONE關聯表示當前模型擁有一個子對象,例如,每個員工都有一個人事檔案。我們可以建立一個用戶模型UserModel,并且添加如下關聯定義:
~~~
namespace Home\Model;
use Think\Model\RelationModel;
class UserModel extends RelationModel{
protected $_link = array(
'Profile'=> self::HAS_ONE,
);
}
~~~
上面是最簡單的方式,表示其遵循了系統內置的數據庫規范,完整的定義方式是:
~~~
namespace Home\Model;
use Think\Model\RelationModel;
class UserModel extends RelationModel{
protected $_link = array(
'Profile'=>array(
'mapping_type' => self::HAS_ONE,
'class_name' => 'Profile',
// 定義更多的關聯屬性
……
),
);
}
~~~
關聯HAS_ONE支持的關聯屬性有:
**mapping_type :關聯類型**
這個在HAS_ONE 關聯里面必須使用HAS_ONE 常量定義。
**class_name :要關聯的模型類名**
例如,class_name 定義為Profile的話則表示和另外的Profile模型類關聯,這個Profile模型類是無需定義的,系統會自動定位到相關的數據表進行關聯。
**mapping_name :關聯的映射名稱,用于獲取數據用**
該名稱不要和當前模型的字段有重復,否則會導致關聯數據獲取的沖突。如果mapping_name沒有定義的話,會取class_name的定義作為mapping_name。如果class_name也沒有定義,則以數組的索引作為mapping_name。
**foreign_key : 關聯的外鍵名稱**
外鍵的默認規則是當前數據對象名稱_id,例如: UserModel對應的可能是表think_user (注意:think只是一個表前綴,可以隨意配置) 那么think_user表的外鍵默認為 user_id,如果不是,就必須在定義關聯的時候顯式定義 foreign_key 。
**condition : 關聯條件**
關聯查詢的時候會自動帶上外鍵的值,如果有額外的查詢條件,可以通過定義關聯的condition屬性。
**mapping_fields : 關聯要查詢的字段**
默認情況下,關聯查詢的關聯數據是關聯表的全部字段,如果只是需要查詢個別字段,可以定義關聯的mapping_fields屬性。
**as_fields :直接把關聯的字段值映射成數據對象中的某個字段**
這個特性是ONE_TO_ONE 關聯特有的,可以直接把關聯數據映射到數據對象中,而不是作為一個關聯數據。當關聯數據的字段名和當前數據對象的字段名稱有沖突時,還可以使用映射定義。
#### BELONGS_TO
Belongs_to 關聯表示當前模型從屬于另外一個父對象,例如每個用戶都屬于一個部門。我們可以做如下關聯定義。
~~~
'Dept' => self::BELONGS_TO
~~~
完整方式定義為:
~~~
'Dept' => array(
'mapping_type' => self::BELONGS_TO,
'class_name' => 'Dept',
'foreign_key' => 'userId',
'mapping_name' => 'dept',
// 定義更多的關聯屬性
……
),
~~~
關聯BELONGS_TO定義支持的關聯屬性有:
| 屬性 | 描述 |
|-----|-----|
| class_name | 要關聯的模型類名 |
| mapping_name | 關聯的映射名稱,用于獲取數據用 該名稱不要和當前模型的字段有重復,否則會導致關聯數據獲取的沖突。 |
| foreign_key | 關聯的外鍵名稱 |
| mapping_fields | 關聯要查詢的字段 |
| condition | 關聯條件 |
| parent_key | 自引用關聯的關聯字段 默認為parent_id 自引用關聯是一種比較特殊的關聯,也就是關聯表就是當前表。 |
| as_fields | 直接把關聯的字段值映射成數據對象中的某個字段 |
#### HAS_MANY
HAS_MANY 關聯表示當前模型擁有多個子對象,例如每個用戶有多篇文章,我們可以這樣來定義:
~~~
'Article' => self::HAS_MANY
~~~
完整定義方式為:
~~~
'Article' => array(
'mapping_type' => self::HAS_MANY,
'class_name' => 'Article',
'foreign_key' => 'userId',
'mapping_name' => 'articles',
'mapping_order' => 'create_time desc',
// 定義更多的關聯屬性
……
),
~~~
關聯HAS_MANY定義支持的關聯屬性有:
| 屬性 | 描述 |
|-----|-----|
| class_name | 要關聯的模型類名 |
| mapping_name | 關聯的映射名稱,用于獲取數據用 該名稱不要和當前模型的字段有重復,否則會導致關聯數據獲取的沖突。 |
| foreign_key | 關聯的外鍵名稱 |
| parent_key | 自引用關聯的關聯字段 默認為parent_id |
| condition | 關聯條件 關聯查詢的時候會自動帶上外鍵的值,如果有額外的查詢條件,可以通過定義關聯的condition屬性。 |
| mapping_fields | 關聯要查詢的字段 默認情況下,關聯查詢的關聯數據是關聯表的全部字段,如果只是需要查詢個別字段,可以定義關聯的mapping_fields屬性。 |
| mapping_limit | 關聯要返回的記錄數目 |
| mapping_order | 關聯查詢的排序 |
外鍵的默認規則是當前數據對象名稱_id,例如:UserModel對應的可能是表think_user (注意:think只是一個表前綴,可以隨意配置) 那么think_user表的外鍵默認為 user_id,如果不是,就必須在定義關聯的時候定義 foreign_key 。
#### MANY_TO_MANY
MANY_TO_MANY 關聯表示當前模型可以屬于多個對象,而父對象則可能包含有多個子對象,通常兩者之間需要一個中間表類約束和關聯。例如每個用戶可以屬于多個組,每個組可以有多個用戶:
~~~
'Group' => self::MANY_TO_MANY
~~~
完整定義方式為:
~~~
'Group' => array(
'mapping_type' => self::MANY_TO_MANY,
'class_name' => 'Group',
'mapping_name' => 'groups',
'foreign_key' => 'userId',
'relation_foreign_key' => 'groupId',
'relation_table' => 'think_group_user' //此處應顯式定義中間表名稱,且不能使用C函數讀取表前綴
)
~~~
`MANY_TO_MANY`支持的關聯屬性定義有:
| 屬性 | 描述 |
|-----|-----|
| class_name | 要關聯的模型類名 |
| mapping_name | 關聯的映射名稱,用于獲取數據用 該名稱不要和當前模型的字段有重復,否則會導致關聯數據獲取的沖突。 |
| foreign_key | 關聯的外鍵名稱 外鍵的默認規則是當前數據對象名稱_id |
| relation_foreign_key | 關聯表的外鍵名稱 默認的關聯表的外鍵名稱是表名_id |
| mapping_limit | 關聯要返回的記錄數目 |
| mapping_order | 關聯查詢的排序 |
| relation_table | 多對多的中間關聯表名稱 |
多對多的中間表默認表規則是:**`數據表前綴_關聯操作的主表名_關聯表名`**
如果think_user 和 think_group 存在一個對應的中間表,默認的表名應該是 如果是由group來操作關聯表,中間表應該是 think_group_user,如果是從user表來操作,那么應該是think_user_group,也就是說,多對多關聯的設置,必須有一個Model類里面需要顯式定義中間表,否則雙向操作會出錯。 中間表無需另外的id主鍵(但是這并不影響中間表的操作),通常只是由 user_id 和 group_id 構成。 默認會通過當前模型的getRelationTableName方法來自動獲取,如果當前模型是User,關聯模型是Group,那么關聯表的名稱也就是使用 user_group這樣的格式,如果不是默認規則,需要指定relation_table屬性。
**3.2.2版本**開始,relation_table定義支持簡化寫法,例如:
~~~
'relation_table'=>'__USER_GROUP__'
~~~
### 關聯查詢
由于性能問題,新版取消了自動關聯查詢機制,而統一使用relation方法進行關聯操作,relation方法不但可以啟用關聯還可以控制局部關聯操作,實現了關聯操作一切盡在掌握之中。
~~~
$User = D("User");
$user = $User->relation(true)->find(1);
~~~
輸出$user結果可能是類似于下面的數據:
~~~
array(
'id' => 1,
'account' => 'ThinkPHP',
'password' => '123456',
'Profile' => array(
'email' => 'liu21st@gmail.com',
'nickname' => '流年',
),
)
~~~
我們可以看到,用戶的關聯數據已經被映射到數據對象的屬性里面了。其中Profile就是關聯定義的mapping_name屬性。
如果我們按照下面的方式定義了as_fields屬性的話,
~~~
protected $_link = array(
'Profile'=>array(
'mapping_type' => self::HAS_ONE,
'class_name' => 'Profile',
'foreign_key' => 'userId',
'as_fields' => 'email,nickname',
),
);
~~~
查詢的結果就變成了下面的結果
~~~
array(
'id' => 1,
'account' => 'ThinkPHP',
'password' => 'name',
'email' => 'liu21st@gmail.com',
'nickname' => '流年',
)
~~~
email和nickname兩個字段已經作為user數據對象的字段來顯示了。
如果關聯數據的字段名和當前數據對象的字段有沖突的話,怎么解決呢?
我們可以用下面的方式來變化下定義:
~~~
'as_fields' => 'email,nickname:username',
~~~
表示關聯表的nickname字段映射成當前數據對象的username字段。
默認會把所有定義的關聯數據都查詢出來,有時候我們并不希望這樣,就可以給relation方法傳入參數來控制要關聯查詢的。
~~~
$User = D("User");
$user = $User->relation('Profile')->find(1);
~~~
關聯查詢一樣可以支持select方法,如果要查詢多個數據,并同時獲取相應的關聯數據,可以改成:
~~~
$User = D("User");
$list = $User->relation(true)->Select();
~~~
如果希望在完成的查詢基礎之上 再進行關聯數據的查詢,可以使用
~~~
$User = D("User");
$user = $User->find(1);
// 表示對當前查詢的數據對象進行關聯數據獲取
$profile = $User->relationGet("Profile");
~~~
事實上,除了當前的參考模型User外,其他的關聯模型是不需要創建的。
### 關聯操作
除了關聯查詢外,系統也支持關聯數據的自動寫入、更新和刪除
#### 關聯寫入
~~~
$User = D("User");
$data = array();
$data["account"] = "ThinkPHP";
$data["password"] = "123456";
$data["Profile"] = array(
'email' =>'liu21st@gmail.com',
'nickname' =>'流年',
);
$result = $User->relation(true)->add($data);
~~~
這樣就會自動寫入關聯的Profile數據。
同樣,可以使用參數來控制要關聯寫入的數據:
~~~
$result = $User->relation("Profile")->add($data);
~~~
> 當MANY_TO_MANY時,不建議使用關聯插入。
#### 關聯更新
數據的關聯更新和關聯寫入類似
~~~
$User = D("User");
$data["account"] = "ThinkPHP";
$data["password"] = "123456";
$data["Profile"] = array(
'email' =>'liu21st@gmail.com',
'nickname' =>'流年',
);
$result = $User-> relation(true)->where(array('id'=>3))->save($data);
~~~
Relation(true)會關聯保存User模型定義的所有關聯數據,如果只需要關聯保存部分數據,可以使用:
~~~
$result = $User->relation("Profile")->save($data);
~~~
這樣就只會同時更新關聯的Profile數據。
關聯保存的規則:
**HAS_ONE**: 關聯數據的更新直接賦值
**HAS_MANY**: 的關聯數據如果傳入主鍵的值 則表示更新 否則就表示新增
**MANY_TO_MANY**: 的數據更新是刪除之前的數據后重新寫入
#### 關聯刪除
~~~
//刪除用戶ID為3的記錄的同時刪除關聯數據
$result = $User->relation(true)->delete("3");
// 如果只需要關聯刪除部分數據,可以使用
$result = $User->relation("Profile")->delete("3");
~~~
## 高級模型
高級模型提供了更多的查詢功能和模型增強功能,利用了模型類的擴展機制實現。如果需要使用高級模型的下面這些功能,記得需要繼承Think\Model\AdvModel類或者采用動態模型。
~~~
namespace Home\Model;
use Think\Model\AdvModel;
class UserModel extends AdvModel{
}
~~~
我們下面的示例都假設UserModel類繼承自Think\Model\AdvModel類。
### 字段過濾
基礎模型類內置有數據自動完成功能,可以對字段進行過濾,但是必須通過Create方法調用才能生效。高級模型類的字段過濾功能卻可以不受create方法的調用限制,可以在模型里面定義各個字段的過濾機制,包括寫入過濾和讀取過濾。
字段過濾的設置方式只需要在Model類里面添加 `$_filter`屬性,并且加入過濾因子,格式如下:
~~~
protected $_filter = array(
'過濾的字段'=>array('寫入過濾規則','讀取過濾規則',是否傳入整個數據對象),
)
~~~
過濾的規則是一個函數,如果設置傳入整個數據對象,那么函數的參數就是整個數據對象,默認是傳入數據對象中該字段的值。
舉例說明,例如我們需要在發表文章的時候對文章內容進行安全過濾,并且希望在讀取的時候進行截取前面255個字符,那么可以設置:
~~~
protected $_filter = array(
'content'=>array('contentWriteFilter','contentReadFilter'),
)
~~~
其中,contentWriteFilter是自定義的對字符串進行安全過濾的函數,而contentReadFilter是自定義的一個對內容進行截取的函數。通常我們可以在項目的公共函數文件里面定義這些函數。
## 序列化字段
序列化字段是新版推出的新功能,可以用簡單的數據表字段完成復雜的表單數據存儲,尤其是動態的表單數據字段。 要使用序列化字段的功能,只需要在模型中定義serializeField屬性,定義格式如下:
~~~
protected $serializeField = array(
'info' => array('name', 'email', 'address'),
);
~~~
Info是數據表中的實際存在的字段,保存到其中的值是name、email和address三個表單字段的序列化結果。序列化字段功能可以在數據寫入的時候進行自動序列化,并且在讀出數據表的時候自動反序列化,這一切都無需手動進行。
下面還是是User數據表為例,假設其中并不存在name、email和address字段,但是設計了一個文本類型的info字段,那么下面的代碼是可行的:
~~~
$User = D("User"); // 實例化User對象
// 然后直接給數據對象賦值
$User->name = 'ThinkPHP';
$User->email = 'ThinkPHP@gmail.com';
$User->address = '上海徐匯區';
// 把數據對象添加到數據庫 name email和address會自動序列化后保存到info字段
$User->add();
查詢用戶數據信息
$User->find(8);
// 查詢結果會自動把info字段的值反序列化后生成name、email和address屬性
// 輸出序列化字段
echo $User->name;
echo $User->email;
echo $User->address;
~~~
## 文本字段
ThinkPHP支持數據模型中的個別字段采用文本方式存儲,這些字段就稱為文本字段,通常可以用于某些Text或者Blob字段,或者是經常更新的數據表字段。
要使用文本字段非常簡單,只要在模型里面定義blobFields屬性就行了。例如,我們需要對Blog模型的content字段使用文本字段,那么就可以使用下面的定義:
~~~
Protected $blobFields = array('content');
~~~
系統在查詢和寫入數據庫的時候會自動檢測文本字段,并且支持多個字段的定義。
> 需要注意的是:對于定義的文本字段并不需要數據庫有對應的字段,完全是另外的。而且,暫時不支持對文本字段的搜索功能。
## 只讀字段
只讀字段用來保護某些特殊的字段值不被更改,這個字段的值一旦寫入,就無法更改。 要使用只讀字段的功能,我們只需要在模型中定義readonlyField屬性
~~~
protected $readonlyField = array('name', 'email');
~~~
例如,上面定義了當前模型的name和email字段為只讀字段,不允許被更改。也就是說當執行save方法之前會自動過濾到只讀字段的值,避免更新到數據庫。
下面舉個例子說明下:
~~~
$User = D("User"); // 實例化User對象
$User->find(8);
// 更改某些字段的值
$User->name = 'TOPThink';
$User->email = 'Topthink@gmail.com';
$User->address = '上海靜安區';
// 保存更改后的用戶數據
$User->save();
~~~
事實上,由于我們對name和email字段設置了只讀,因此只有address字段的值被更新了,而name和email的值仍然還是更新之前的值。
## 悲觀鎖和樂觀鎖
業務邏輯的實現過程中,往往需要保證數據訪問的排他性。如在金融系統的日終結算處理中,我們希望針對某個時間點的數據進行處理,而不希望在結算進行過程中(可能是幾秒種,也可能是幾個小時),數據再發生變化。此時,我們就需要通過一些機制來保證這些數據在某個操作過程中不會被外界修改,這樣的機制,在這里,也就是所謂的 “ 鎖 ” ,即給我們選定的目標數據上鎖,使其無法被其他程序修改。 ThinkPHP支持兩種鎖機制:即通常所說的 “ 悲觀鎖( Pessimistic Locking ) ”和 “ 樂觀鎖( Optimistic Locking ) ” 。
### 悲觀鎖( Pessimistic Locking )
悲觀鎖,正如其名,它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處于鎖定狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。 通常是使用for update子句來實現悲觀鎖機制。
ThinkPHP支持悲觀鎖機制,默認情況下,是關閉悲觀鎖功能的,要在查詢和更新的時候啟用悲觀鎖功能,可以通過使用之前提到的查詢鎖定方法,例如:
~~~
$User->lock(true)->save($data);// 使用悲觀鎖功能
~~~
### 樂觀鎖( Optimistic Locking )
相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。 如一個金融系統,當某個操作員讀取用戶的數據,并在讀出的用戶數據的基礎上進行修改時(如更改用戶帳戶余額),如果采用悲觀鎖機制,也就意味著整個操作過程中(從操作員讀出數據、開始修改直至提交修改結果的全過程,甚至還包括操作員中途去煮咖啡的時間),數據庫記錄始終處于加鎖狀態,可以想見,如果面對幾百上千個并發,這樣的情況將導致怎樣的后果。樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基于數據版本( Version )記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基于數據庫表的版本解決方案中,一般是通過為數據庫表增加一個 “version” 字段來實現。
ThinkPHP也可以支持樂觀鎖機制,要啟用樂觀鎖,只需要繼承高級模型類并定義模型的optimLock屬性,并且在數據表字段里面增加相應的字段就可以自動啟用樂觀鎖機制了。默認的optimLock屬性是lock_version,也就是說如果要在User表里面啟用樂觀鎖機制,只需要在User表里面增加lock_version字段,如果有已經存在的其它字段作為樂觀鎖用途,可以修改模型類的optimLock屬性即可。如果存在optimLock屬性對應的字段,但是需要臨時關閉樂觀鎖機制,把optimLock屬性設置為false就可以了。
## 數據分表
對于大數據量的應用,經常會對數據進行分表,有些情況是可以利用數據庫的分區功能,但并不是所有的數據庫或者版本都支持,因此我們可以利用ThinkPHP內置的數據分表功能來實現。幫助我們更方便的進行數據的分表和讀取操作。
和數據庫分區功能不同,內置的數據分表功能需要根據分表規則手動創建相應的數據表。
在需要分表的模型中定義partition屬性即可。
~~~
protected $partition = array(
'field' => 'name',// 要分表的字段 通常數據會根據某個字段的值按照規則進行分表
'type' => 'md5',// 分表的規則 包括id year mod md5 函數 和首字母
'expr' => 'name',// 分表輔助表達式 可選 配合不同的分表規則
'num' => 'name',// 分表的數目 可選 實際分表的數量
);
~~~
定義好了分表屬性后,我們就可以來進行CURD操作了,唯一不同的是,獲取當前的數據表不再使用getTableName方法,而是使用getPartitionTableName方法,而且必須傳入當前的數據。然后根據數據分析應該實際操作哪個數據表。因此,分表的字段值必須存在于傳入的數據中,否則會進行聯合查詢。
## 返回類型
系統默認的數據庫查詢返回的是數組,我們可以給單個數據設置返回類型,以滿足特殊情況的需要,例如:
~~~
$User = M("User"); // 實例化User對象
// 返回結果是一個數組數據
$data = $User->find(6);
// 返回結果是一個stdClass對象
$data = $User->returnResult($data, "object");
// 還可以返回自定義的類
$data = $User->returnResult($data, "User");
~~~
返回自定義的User類,類的架構方法的參數是傳入的數據。例如:
~~~
Class User {
public function __construct($data){
// 對$data數據進行處理
}
}
~~~
上面是官方的介紹,老楊被人反饋說分表手冊不詳細,難以實踐,我就研究了下。加入了演示。
1. 分表的表名規則 `XXX_table_序號` 如 'adv_article_1'。
2. 分表的表結構必須一樣,有軟件的建好第一個表后,復制表給個新表名就行了
3. 寫一個繼承高級模型的模型類,關閉字段緩存。
4. 模型類里提供一個方法進行表名切換。
5. 進行分表查詢和插入。
首先看一下效果:
。
然后看下我的數據庫:



成功的將第3條數據插入了`adv_article_2`里。
那么我是如何做的呢?
先建2張表:
~~~
DROP TABLE IF EXISTS `adv_article_1`;
CREATE TABLE `adv_article_1` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`title` varchar(255) NOT NULL COMMENT '標題',
`content` varchar(1000) DEFAULT NULL COMMENT '內容',
`time` int(11) unsigned DEFAULT '0' COMMENT '添加時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for `adv_article_2`
-- ----------------------------
DROP TABLE IF EXISTS `adv_article_2`;
CREATE TABLE `adv_article_2` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`title` varchar(255) NOT NULL COMMENT '標題',
`content` varchar(1000) DEFAULT NULL COMMENT '內容',
`time` int(11) unsigned DEFAULT '0' COMMENT '添加時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
~~~
然后是模型 AdvArticleModel:
~~~
<?php
namespace Home\Model;
use Think\Model\AdvModel;
class AdvArticleModel extends AdvModel{
protected $tableName = 'article';
Protected $autoCheckFields = false;
protected $partition = array(
'field' => 'id',// 要分表的字段 通常數據會根據某個字段的值按照規則進行分表
'type' => 'id',// 分表的規則 包括id year mod md5 函數 和首字母
'expr' => '3',// 分表輔助表達式 可選 配合不同的分表規則
'num' => '2',// 分表的數目 可選 實際分表的數量
);
//獲取多表表名
public function init($data = array()){
$data = empty($data) ? $_POST : $data;
$table = $this->getPartitionTableName($data);
if(!empty($data)){
dump("實際的表名:{$table}");
}
return $this->table($table);
}
//清空多表
public function emptyTable(){
$tableName = array();
for($i=0;$i<$this->partition['num'];$i++){
$tableName = $this->getTableName().'_'.($i+1);
$this->execute("TRUNCATE TABLE `{$tableName}`");
}
}
}
~~~
`protected $tableName = 'article';` 指明了高級模型的表名。
`Protected $autoCheckFields = false;` 關閉了字段緩存。
~~~
//獲取多表表名
public function init($data = array()){
$data = empty($data) ? $_POST : $data;
$table = $this->getPartitionTableName($data);
if(!empty($data)){
dump("實際的表名:{$table}");
}
return $this->table($table);
}
~~~
獲取多表表名的方法,用于計算真實的分表表名,如果沒有數據進來,就是union的聯合表。如這樣的:`( SELECT * FROM adv_article_1 UNION SELECT * FROM adv_article_2) AS AdvArticle'`。確定了分表名會是這樣的`adv_article_2`。
接下來時清空多表的方法。為了演示方便,我設置了2個分表,然后id大于2的話進入下一個表。所以示列如果不清除數據,刷新后在有數據的情況下會進入表3。而數據庫里沒有這個表就會報錯。
~~~
//清空多表
public function emptyTable(){
$tableName = array();
for($i=0;$i<$this->partition['num'];$i++){
$tableName = $this->getTableName().'_'.($i+1);
$this->execute("TRUNCATE TABLE `{$tableName}`");
}
}
~~~
然后看我們的高級模型控制器:
~~~
<?php
namespace Home\Controller;
use Think\Controller;
class AdvmodelController extends Controller {
public function index(){
//分表演示
C('DB_PREFIX', 'adv_');
$article = D('AdvArticle');
$article->emptyTable();//清空多表
for ($i=0; $i < 3; $i++) {
//獲取最新的id供分表識別表名所用
$id = $article->init()->max('id');
$id = intval($id);
$targetId = $id+1;
$this->show('準備插入的數據:');
$data = array('title'=>'標題'.$key, 'content'=>'內容'.$key, 'time'=>time(), 'id'=>$targetId);
dump($data);
$advModel = $article->init($data);
$result = $advModel->add($data);
if($result){
$this->show('插入數據后的id為:'.$result);
}else{
$this->show('插入id為'.$targetId.'的數據失敗,失敗原因:'.$advModel->getError());
}
$this->show('<br><br>');
}
$allData = $article->init()->select();
var_dump($allData);
var_dump($allData, 'object');
}
}
~~~
~~~
$article = D('AdvArticle');
$article->emptyTable();//清空多表
~~~
實列化高級模型。
~~~
for ($i=0; $i < 3; $i++) {
//獲取最新的id供分表識別表名所用
$id = $article->init()->max('id');
$id = intval($id);
$targetId = $id+1;
$this->show('準備插入的數據:');
$data = array('title'=>'標題'.$key, 'content'=>'內容'.$key, 'time'=>time(), 'id'=>$targetId);
dump($data);
$advModel = $article->init($data);
$result = $advModel->add($data);
if($result){
$this->show('插入數據后的id為:'.$result);
}else{
$this->show('插入id為'.$targetId.'的數據失敗,失敗原因:'.$advModel->getError());
}
$this->show('<br><br>');
}
~~~
偽造數據,并添加數據。注意不要以為id是主鍵自增,就不傳入init里去獲取分表名。一定要傳你設置的$partition 分表屬性的field里的值在數組里。
~~~
protected $partition = array(
'field' => 'id',// 要分表的字段 通常數據會根據某個字段的值按照規則進行分表
'type' => 'id',// 分表的規則 包括id year mod md5 函數 和首字母
'expr' => '3',// 分表輔助表達式 可選 配合不同的分表規則
'num' => '2',// 分表的數目 可選 實際分表的數量
);
~~~
主要這個里面 其他的鍵如field、type、expr大家可能不懂。
num就是你有幾個分表。
其實看下源碼就能明白了。
~~~
/**
* 得到分表的的數據表名
* @access public
* @param array $data 操作的數據
* @return string
*/
public function getPartitionTableName($data=array()) {
// 對數據表進行分區
if(isset($data[$this->partition['field']])) {
$field = $data[$this->partition['field']];
switch($this->partition['type']) {
case 'id':
// 按照id范圍分表
$step = $this->partition['expr'];
$seq = floor($field / $step)+1;
break;
case 'year':
// 按照年份分表
if(!is_numeric($field)) {
$field = strtotime($field);
}
$seq = date('Y',$field)-$this->partition['expr']+1;
break;
case 'mod':
// 按照id的模數分表
$seq = ($field % $this->partition['num'])+1;
break;
case 'md5':
// 按照md5的序列分表
$seq = (ord(substr(md5($field),0,1)) % $this->partition['num'])+1;
break;
default :
if(function_exists($this->partition['type'])) {
// 支持指定函數哈希
$fun = $this->partition['type'];
$seq = (ord(substr($fun($field),0,1)) % $this->partition['num'])+1;
}else{
// 按照字段的首字母的值分表
$seq = (ord($field{0}) % $this->partition['num'])+1;
}
}
return $this->getTableName().'_'.$seq;
}else{
// 當設置的分表字段不在查詢條件或者數據中
// 進行聯合查詢,必須設定 partition['num']
$tableName = array();
for($i=0;$i<$this->partition['num'];$i++)
$tableName[] = 'SELECT * FROM '.$this->getTableName().'_'.($i+1);
$tableName = '( '.implode(" UNION ",$tableName).') AS '.$this->name;
return $tableName;
}
}
~~~
我就分別解釋下`switch($this->partition['type'])`中的type:
- id 就是拿id除每個表總數量的商取不超過的整數+1。比方3個記錄,每個表最多2條那么就按照 1 2,3 存2個表里,所以id 得程序算出來。
- year 就是根據分表字段來算一個四位數的年份,然后expr 是多少年為間隔存一個表。所以該字段要么是時間格式的,要么是時間戳。
- mod 取模,這個簡單,就是數學上的取模,`1234` 取模后對應為`1010`,那么 1 2 3 4 字段對應的表就為+1 后的 2 1 2 1 分表里。
- md5 就是將字段的值md5變32位字符后取首字母在拿首字母對應的 ASCII 碼值 去模分表個數。其實就是高級取模。
- default,就是type不是上面的形式時,就當自定義函數處理,如果函數存在,拿函數處理的返回值高級取模,不存在直接拿字段高級取模處理分表名。
如果獲取表名沒傳data或者data里不存在field鍵時。就union 返回一個別名 查出所有分表的數據。
至于分表處理的`returnResult`,無非就支持了對象返回和以一個類的構造方法去返回數據處理,相當于模型的后置查詢操作。
~~~
/**
* 返回數據
* @access public
* @param array $data 數據
* @param string $type 返回類型 默認為數組
* @return mixed
*/
public function returnResult($data,$type='') {
if('' === $type)
$type = $this->returnType;
switch($type) {
case 'array' : return $data;
case 'object': return (object)$data;
default:// 允許用戶自定義返回類型
if(class_exists($type))
return new $type($data);
else
E(L('_CLASS_NOT_EXIST_').':'.$type);
}
}
~~~
源碼也就這點,沒啥好講的。其實分表的關鍵就是建對表,社對屬性。定好分表規則,然后查出對應表。進行分表處理后插入和讀取。其他和基本模型沒區別。注意關閉字段緩存,不然光實例化模型就會報表名不存在了。
- 序
- 前言
- 內容簡介
- 目錄
- 基礎知識
- 起步
- 控制器
- 模型
- 模板
- 命名空間
- 進階知識
- 路由
- 配置
- 緩存
- 權限
- 擴展
- 國際化
- 安全
- 單元測試
- 拿來主義
- 調試方法
- 調試的步驟
- 調試工具
- 顯示trace信息
- 開啟調試和關閉調試的區別
- netbeans+xdebug
- Socketlog
- PHP常見錯誤
- 小黃鴨調試法,每個程序員都要知道的
- 應用場景
- 第三方登錄
- 圖片處理
- 博客
- SAE
- REST實踐
- Cli
- ajax分頁
- barcode條形碼
- excel
- 發郵件
- 漢字轉全拼和首字母,支持帶聲調
- 中文分詞
- 瀏覽器useragent解析
- freelog項目實戰
- 需求分析
- 數據庫設計
- 編碼實踐
- 前端實現
- rest接口
- 文章發布
- 文件上傳
- 視頻播放
- 音樂播放
- 圖片幻燈片展示
- 注冊和登錄
- 個人資料更新
- 第三方登錄的使用
- 后臺
- 微信的開發
- 首頁及個人主頁
- 列表
- 歸檔
- 搜索
- 分頁
- 總結經驗
- 自我提升
- 進行小項目的鍛煉
- 對現有輪子的重構和移植
- 寫技術博客
- 制作視頻教程
- 學習PHP的知識和新特性
- 和同行直接溝通、交流
- 學好英語,走向國際
- 如何參與
- 瀏覽官網和極思維還有看云
- 回答ThinkPHP新手的問題
- 嘗試發現ThinkPHP的bug,告訴官方人員或者push request
- 開發能提高效率的ThinkPHP工具
- 嘗試翻譯官方文檔
- 幫新手入門
- 創造基于ThinkPHP的產品,進行連帶推廣
- 展望未來
- OneThink
- ThinkPHP4
- 附錄