摘要:建立關(guān)聯(lián)關(guān)系后,通過(guò)可以獲取一個(gè)對(duì)象的數(shù)組,該數(shù)組代表當(dāng)前客戶(hù)對(duì)象的訂單集。定義關(guān)聯(lián)關(guān)系使用一個(gè)可以返回對(duì)象的方法,對(duì)象有關(guān)聯(lián)上下文的相關(guān)信息,因此可以只查詢(xún)關(guān)聯(lián)數(shù)據(jù)?;诒硗怄I定義關(guān)聯(lián)關(guān)系是最佳方法。
簡(jiǎn)介
Yii 在操作數(shù)據(jù)庫(kù)方面提供了一個(gè)十分強(qiáng)大的類(lèi)庫(kù)來(lái)支撐整個(gè)框架業(yè)務(wù)的運(yùn)轉(zhuǎn),這就是 Active Record (活動(dòng)記錄,以下簡(jiǎn)稱(chēng)AR)。
基本概念AR類(lèi)提供了一個(gè)面向?qū)ο蟮慕涌冢?用以訪問(wèn)數(shù)據(jù)庫(kù)中的數(shù)據(jù)。
例如,假定 Customer AR 類(lèi)關(guān)聯(lián)著 customer 表,且該類(lèi)的 name 屬性代表 customer 表的 name 列。 你可以寫(xiě)以下代碼來(lái)哉customer 表里插入一行新的記錄:
用 AR 而不是原生的 SQL 語(yǔ)句去執(zhí)行數(shù)據(jù)庫(kù)查詢(xún),可以調(diào)用直觀方法來(lái)實(shí)現(xiàn)相同目標(biāo)。
如,調(diào)用 yiidbActiveRecord::save() 方法將執(zhí)行插入或更新輪詢(xún),將在該 AR 類(lèi)關(guān)聯(lián)的數(shù)據(jù)表新建或更新一行數(shù)據(jù):
$customer = new Customer(); $customer->name = "Qiang"; $customer->save(); // 一行新數(shù)據(jù)插入 customer 表
上面的代碼和使用下面的原生 SQL 語(yǔ)句是等效的,但顯然前者更直觀, 更不易出錯(cuò),并且面對(duì)不同的數(shù)據(jù)庫(kù)系統(tǒng)(DBMS, Database Management System)時(shí)更不容易產(chǎn)生兼容性問(wèn)題。
$db->createCommand("INSERT INTO customer (name) VALUES (:name)", [ ":name" => "Qiang", ])->execute();
下面是所有目前被 Yii 的 AR 功能所支持的數(shù)據(jù)庫(kù)列表:
MySQL 4.1 及以上:通過(guò) yiidbActiveRecord
PostgreSQL 7.3 及以上:通過(guò) yiidbActiveRecord
SQLite 2 和 3:通過(guò) yiidbActiveRecord
Microsoft SQL Server 2010 及以上:通過(guò) yiidbActiveRecord
Oracle: 通過(guò) yiidbActiveRecord
CUBRID 9.1 及以上:通過(guò) yiidbActiveRecord
Sphinx:通過(guò) yiisphinxActiveRecord,需求 yii2-sphinx 擴(kuò)展
ElasticSearch:通過(guò) yiielasticsearchActiveRecord,需求 yii2-elasticsearch 擴(kuò)展
Redis 2.6.12 及以上:通過(guò) yiiredisActiveRecord,需求 yii2-redis 擴(kuò)展
MongoDB 1.3.0 及以上:通過(guò) yiimongodbActiveRecord,需求 yii2-mongodb 擴(kuò)展
如你所見(jiàn),Yii 不僅提供了對(duì)關(guān)系型數(shù)據(jù)庫(kù)的 AR 支持,還提供了 NoSQL 數(shù)據(jù)庫(kù)的支持。
聲明 AR 類(lèi)要想聲明一個(gè) AR 類(lèi),你需要擴(kuò)展 yiidbActiveRecord 基類(lèi), 并實(shí)現(xiàn) tableName 方法,返回與之相關(guān)聯(lián)的的數(shù)據(jù)表的名稱(chēng):
namespace appmodels; use yiidbActiveRecord; class Customer extends ActiveRecord{ /** * @return string 返回該AR類(lèi)關(guān)聯(lián)的數(shù)據(jù)表名 */ public static function tableName() { return "customer"; } }訪問(wèn)列數(shù)據(jù)
AR 把相應(yīng)數(shù)據(jù)行的每一個(gè)字段映射為 AR 對(duì)象的一個(gè)個(gè)特性變量(Attribute) 一個(gè)特性就好像一個(gè)普通對(duì)象的公共屬性一樣(public property)。
特性變量的名稱(chēng)和對(duì)應(yīng)字段的名稱(chēng)是一樣的,且大小姓名。
使用以下語(yǔ)法讀取列的值:
// "id" 和 "mail" 是 $customer 對(duì)象所關(guān)聯(lián)的數(shù)據(jù)表的對(duì)應(yīng)字段名 $id = $customer->id; $email = $customer->email;
要改變列值,只要給關(guān)聯(lián)屬性賦新值并保存對(duì)象即可:
$customer->email = "[email protected]"; $customer->save();建立數(shù)據(jù)庫(kù)連接
AR 用一個(gè) yiidbConnection 對(duì)象與數(shù)據(jù)庫(kù)交換數(shù)據(jù)。
默認(rèn)的,它使用 db 組件作為其連接對(duì)象,你可以在應(yīng)用程序配置文件中設(shè)置下 db 組件,就像這樣:
return [ "components" => [ "db" => [ "class" => "yiidbConnection", "dsn" => "mysql:host=localhost;dbname=testdb", "username" => "demo", "password" => "demo", ], ], ];
如果在你的應(yīng)用中應(yīng)用了不止一個(gè)數(shù)據(jù)庫(kù),且你需要給你的 AR 類(lèi)使用不同的數(shù)據(jù)庫(kù)鏈接(DB connection) ,你可以覆蓋掉 yiidbActiveRecord::getDb() 方法:
class Customer extends ActiveRecord{ // ... public static function getDb() { return Yii::$app->db2; // 使用名為 "db2" 的應(yīng)用組件 } }查詢(xún)數(shù)據(jù)
AR 提供了兩種方法來(lái)構(gòu)建 DB 查詢(xún)并向 AR 實(shí)例里填充數(shù)據(jù):
yiidbActiveRecord::find()
yiidbActiveRecord::findBySql()
以上兩個(gè)方法都會(huì)返回 yiidbActiveQuery 實(shí)例,該類(lèi)繼承自yiidbQuery, 因此,他們都支持同一套靈活且強(qiáng)大的 DB 查詢(xún)方法,如where(),join(),orderBy(),等等。
下面的這些案例展示了一些可能的玩法:
// 取回所有活躍客戶(hù)(狀態(tài)為 *active* 的客戶(hù))并以他們的 ID 排序: $customers = Customer::find() ->where(["status" => Customer::STATUS_ACTIVE]) ->orderBy("id") ->all(); // 返回ID為1的客戶(hù): $customer = Customer::find() ->where(["id" => 1]) ->one(); // 取回活躍客戶(hù)的數(shù)量: $count = Customer::find() ->where(["status" => Customer::STATUS_ACTIVE]) ->count(); // 以客戶(hù)ID索引結(jié)果集: // $customers 數(shù)組以 ID 為索引 $customers = Customer::find()->indexBy("id")->all(); // 用原生 SQL 語(yǔ)句檢索客戶(hù): $sql = "SELECT * FROM customer"; $customers = Customer::findBySql($sql)->all();
小技巧:在上面的代碼中,Customer::STATUS_ACTIVE 是一個(gè)在 Customer 類(lèi)里定義的常量。(譯注:這種常量的值一般都是tinyint)相較于直接在代碼中寫(xiě)死字符串或數(shù)字,使用一個(gè)更有意義的常量名稱(chēng)是一種更好的編程習(xí)慣。
有兩個(gè)快捷方法:findOne 和 findAll() 用來(lái)返回一個(gè)或者一組ActiveRecord實(shí)例。前者返回第一個(gè)匹配到的實(shí)例,后者返回所有。 例如:
// 返回 id 為 1 的客戶(hù) $customer = Customer::findOne(1); // 返回 id 為 1 且狀態(tài)為 *active* 的客戶(hù) $customer = Customer::findOne([ "id" => 1, "status" => Customer::STATUS_ACTIVE, ]); // 返回id為1、2、3的一組客戶(hù) $customers = Customer::findAll([1, 2, 3]); // 返回所有狀態(tài)為 "deleted" 的客戶(hù) $customer = Customer::findAll([ "status" => Customer::STATUS_DELETED, ]);以數(shù)組形式獲取數(shù)據(jù)
有時(shí)候,我們需要處理很大量的數(shù)據(jù),這時(shí)可能需要用一個(gè)數(shù)組來(lái)存儲(chǔ)取到的數(shù)據(jù), 從而節(jié)省內(nèi)存。
你可以用 asArray() 函數(shù)做到這一點(diǎn):
// 以數(shù)組而不是對(duì)象形式取回客戶(hù)信息: $customers = Customer::find() ->asArray() ->all(); // $customers 的每個(gè)元素都是鍵值對(duì)數(shù)組批量獲取數(shù)據(jù)
在 Query Builder(查詢(xún)構(gòu)造器) 里,我們已經(jīng)解釋了當(dāng)需要從數(shù)據(jù)庫(kù)中查詢(xún)大量數(shù)據(jù)時(shí),你可以用 batch query(批量查詢(xún))來(lái)限制內(nèi)存的占用。
你可能也想在 AR 里使用相同的技巧,比如這樣……
// 一次提取 10 個(gè)客戶(hù)信息 foreach (Customer::find()->batch(10) as $customers) { // $customers 是 10 個(gè)或更少的客戶(hù)對(duì)象的數(shù)組 } // 一次提取 10 個(gè)客戶(hù)并一個(gè)一個(gè)地遍歷處理 foreach (Customer::find()->each(10) as $customer) { // $customer 是一個(gè) ”Customer“ 對(duì)象 } // 貪婪加載模式的批處理查詢(xún) foreach (Customer::find()->with("orders")->each() as $customer) { }操作數(shù)據(jù)(CURD)
AR 提供以下方法插入、更新和刪除與 AR 對(duì)象關(guān)聯(lián)的那張表中的某一行:
yiidbActiveRecord::save()
yiidbActiveRecord::insert()
yiidbActiveRecord::update()
yiidbActiveRecord::delete()
AR 同時(shí)提供了一些靜態(tài)方法,可以應(yīng)用在與某 AR 類(lèi)所關(guān)聯(lián)的整張表上。
用這些方法的時(shí)候千萬(wàn)要小心,因?yàn)樗麄冏饔糜谡麖埍恚?/p>
比如,deleteAll() 會(huì)刪除掉表里所有的記錄。
yiidbActiveRecord::updateCounters()
yiidbActiveRecord::updateAll()
yiidbActiveRecord::updateAllCounters()
yiidbActiveRecord::deleteAll()
下面的這些例子里,詳細(xì)展現(xiàn)了如何使用這些方法:
// 插入新客戶(hù)的記錄 $customer = new Customer(); $customer->name = "James"; $customer->email = "[email protected]"; $customer->save(); // 等同于 $customer->insert(); // 更新現(xiàn)有客戶(hù)記錄 $customer = Customer::findOne($id); $customer->email = "[email protected]"; $customer->save(); // 等同于 $customer->update(); // 刪除已有客戶(hù)記錄 $customer = Customer::findOne($id); $customer->delete(); // 刪除多個(gè)年齡大于20,性別為男(Male)的客戶(hù)記錄 Customer::deleteAll( "age > :age AND gender = :gender", [":age" => 20, ":gender" => "M"] ); // 所有客戶(hù)的age(年齡)字段加1: Customer::updateAllCounters(["age" => 1]);
數(shù)據(jù)輸入與有效性驗(yàn)證須知:save() 方法會(huì)調(diào)用 insert() 和 update() 中的一個(gè), 用哪個(gè)取決于當(dāng)前 AR 對(duì)象是不是新對(duì)象(在函數(shù)內(nèi)部,他會(huì)檢查 yiidbActiveRecord::isNewRecord 的值)。
若 AR 對(duì)象是由 new 操作符 初始化出來(lái)的,save() 方法會(huì)在表里插入一條數(shù)據(jù); 如果一個(gè) AR 是由 find() 方法獲取來(lái)的, 則 save() 會(huì)更新表里的對(duì)應(yīng)行記錄。
由于AR繼承自yiibaseModel,所以它同樣也支持Model的數(shù)據(jù)輸入、驗(yàn)證等特性。
例如,你可以聲明一個(gè)rules方法用來(lái)覆蓋掉yiibaseModel::rules()里的;你也可以給AR實(shí)例批量賦值;你也可以通過(guò)調(diào)用yiibaseModel::validate()執(zhí)行數(shù)據(jù)驗(yàn)證。
當(dāng)你調(diào)用 save()、insert()、update() 這三個(gè)方法時(shí),會(huì)自動(dòng)調(diào)用yiibaseModel::validate()方法。如果驗(yàn)證失敗,數(shù)據(jù)將不會(huì)保存進(jìn)數(shù)據(jù)庫(kù)。
下面的例子演示了如何使用AR 獲取/驗(yàn)證用戶(hù)輸入的數(shù)據(jù)并將他們保存進(jìn)數(shù)據(jù)庫(kù):
// 新建一條記錄 $model = new Customer; if ($model->load(Yii::$app->request->post()) && $model->save()) { // 獲取用戶(hù)輸入的數(shù)據(jù),驗(yàn)證并保存 } // 更新主鍵為$id的AR $model = Customer::findOne($id); if ($model === null) { throw new NotFoundHttpException; } if ($model->load(Yii::$app->request->post()) && $model->save()) { // 獲取用戶(hù)輸入的數(shù)據(jù),驗(yàn)證并保存 }讀取默認(rèn)值
你的表列也許定義了默認(rèn)值。有時(shí)候,你可能需要在使用web表單的時(shí)候給AR預(yù)設(shè)一些值。
如果你需要這樣做,可以在顯示表單內(nèi)容前通過(guò)調(diào)用loadDefaultValues()方法來(lái)實(shí)現(xiàn):
loadDefaultValues(); // ... 渲染 $customer 的 HTML 表單 ... ?>AR的生命周期
理解AR的生命周期對(duì)于你操作數(shù)據(jù)庫(kù)非常重要。
生命周期通常都會(huì)有些典型的事件存在。
對(duì)于開(kāi)發(fā)AR的behaviors來(lái)說(shuō)非常有用。
當(dāng)你實(shí)例化一個(gè)新的AR對(duì)象時(shí),我們將獲得如下的生命周期:
1. constructor 2. yiidbActiveRecord::init(): 會(huì)觸發(fā)一個(gè) yiidbActiveRecord::EVENT_INIT 事件
當(dāng)你通過(guò) yiidbActiveRecord::find() 方法查詢(xún)數(shù)據(jù)時(shí),每個(gè)AR實(shí)例都將有以下生命周期:
1. constructor 2. yiidbActiveRecord::init(): 會(huì)觸發(fā)一個(gè) yiidbActiveRecord::EVENT_INIT 事件 3. yiidbActiveRecord::afterFind(): 會(huì)觸發(fā)一個(gè) yiidbActiveRecord::EVENT_AFTER_FIND 事件
當(dāng)通過(guò) yiidbActiveRecord::save() 方法寫(xiě)入或者更新數(shù)據(jù)時(shí), 我們將獲得如下生命周期:
1. yiidbActiveRecord::beforeValidate(): 會(huì)觸發(fā)一個(gè) yiidbActiveRecord::EVENT_BEFORE_VALIDATE 事件 2. yiidbActiveRecord::afterValidate(): 會(huì)觸發(fā)一個(gè) yiidbActiveRecord::EVENT_AFTER_VALIDATE 事件 3. yiidbActiveRecord::beforeSave(): 會(huì)觸發(fā)一個(gè) yiidbActiveRecord::EVENT_BEFORE_INSERT 或 yiidbActiveRecord::EVENT_BEFORE_UPDATE 事件 4. 執(zhí)行實(shí)際的數(shù)據(jù)寫(xiě)入或更新 5. yiidbActiveRecord::afterSave(): 會(huì)觸發(fā)一個(gè) yiidbActiveRecord::EVENT_AFTER_INSERT 或 yiidbActiveRecord::EVENT_AFTER_UPDATE 事件
最后,當(dāng)調(diào)用 yiidbActiveRecord::delete() 刪除數(shù)據(jù)時(shí), 我們將獲得如下生命周期:
1. yiidbActiveRecord::beforeDelete(): 會(huì)觸發(fā)一個(gè) yiidbActiveRecord::EVENT_BEFORE_DELETE 事件 2. 執(zhí)行實(shí)際的數(shù)據(jù)刪除 3. yiidbActiveRecord::afterDelete(): 會(huì)觸發(fā)一個(gè) yiidbActiveRecord::EVENT_AFTER_DELETE 事件查詢(xún)關(guān)聯(lián)的數(shù)據(jù)
使用 AR 方法也可以查詢(xún)數(shù)據(jù)表的關(guān)聯(lián)數(shù)據(jù)(如,選出表A的數(shù)據(jù)可以拉出表B的關(guān)聯(lián)數(shù)據(jù))。
有了 AR, 返回的關(guān)聯(lián)數(shù)據(jù)連接就像連接關(guān)聯(lián)主表的 AR 對(duì)象的屬性一樣。
建立關(guān)聯(lián)關(guān)系后,通過(guò) $customer->orders 可以獲取 一個(gè) Order 對(duì)象的數(shù)組,該數(shù)組代表當(dāng)前客戶(hù)對(duì)象的訂單集。
定義關(guān)聯(lián)關(guān)系使用一個(gè)可以返回 yiidbActiveQuery 對(duì)象的 getter 方法, yiidbActiveQuery對(duì)象有關(guān)聯(lián)上下文的相關(guān)信息,因此可以只查詢(xún)關(guān)聯(lián)數(shù)據(jù)。
例如:
class Customer extends yiidbActiveRecord{ public function getOrders() { // 客戶(hù)和訂單通過(guò) Order.customer_id -> id 關(guān)聯(lián)建立一對(duì)多關(guān)系 return $this->hasMany(Order::className(), ["customer_id" => "id"]); } } class Order extends yiidbActiveRecord{ // 訂單和客戶(hù)通過(guò) Customer.id -> customer_id 關(guān)聯(lián)建立一對(duì)一關(guān)系 public function getCustomer() { return $this->hasOne(Customer::className(), ["id" => "customer_id"]); } }
以上使用了 yiidbActiveRecord::hasMany() 和 yiidbActiveRecord::hasOne() 方法。
以上兩例分別是關(guān)聯(lián)數(shù)據(jù)多對(duì)一關(guān)系和一對(duì)一關(guān)系的建模范例。
如,一個(gè)客戶(hù)有很多訂單,一個(gè)訂單只歸屬一個(gè)客戶(hù)。
兩個(gè)方法都有兩個(gè)參數(shù)并返回 yiidbActiveQuery 對(duì)象。
$class:關(guān)聯(lián)模型類(lèi)名,它必須是一個(gè)完全合格的類(lèi)名。 $link: 兩個(gè)表的關(guān)聯(lián)列,應(yīng)為鍵值對(duì)數(shù)組的形式。 數(shù)組的鍵是 $class 關(guān)聯(lián)表的列名, 而數(shù)組值是關(guān)聯(lián)類(lèi) $class 的列名。 基于表外鍵定義關(guān)聯(lián)關(guān)系是最佳方法。
建立關(guān)聯(lián)關(guān)系后,獲取關(guān)聯(lián)數(shù)據(jù)和獲取組件屬性一樣簡(jiǎn)單, 執(zhí)行以下相應(yīng)getter方法即可:
// 取得客戶(hù)的訂單 $customer = Customer::findOne(1); $orders = $customer->orders; // $orders 是 Order 對(duì)象數(shù)組
以上代碼實(shí)際執(zhí)行了以下兩條 SQL 語(yǔ)句:
SELECT * FROM customer WHERE id=1; SELECT * FROM order WHERE customer_id=1;
提示:再次用表達(dá)式 $customer->orders將不會(huì)執(zhí)行第二次 SQL 查詢(xún), SQL 查詢(xún)只在該表達(dá)式第一次使用時(shí)執(zhí)行。
數(shù)據(jù)庫(kù)訪問(wèn)只返回緩存在內(nèi)部前一次取回的結(jié)果集,如果你想查詢(xún)新的 關(guān)聯(lián)數(shù)據(jù),先要注銷(xiāo)現(xiàn)有結(jié)果集:
unset($customer->orders);。
有時(shí)候需要在關(guān)聯(lián)查詢(xún)中傳遞參數(shù),如不需要返回客戶(hù)全部訂單, 只需要返回購(gòu)買(mǎi)金額超過(guò)設(shè)定值的大訂單, 通過(guò)以下getter方法聲明一個(gè)關(guān)聯(lián)數(shù)據(jù) bigOrders :
class Customer extends yiidbActiveRecord{ public function getBigOrders($threshold = 100) { return $this->hasMany(Order::className(), ["customer_id" => "id"]) ->where("subtotal > :threshold", [":threshold" => $threshold]) ->orderBy("id"); } }
hasMany() 返回 yiidbActiveQuery 對(duì)象,該對(duì)象允許你通過(guò) yiidbActiveQuery 方法定制查詢(xún)。
如上聲明后,執(zhí)行 $customer->bigOrders 就返回 總額大于100的訂單。使用以下代碼更改設(shè)定值:
$orders = $customer->getBigOrders(200)->all();
注意:關(guān)聯(lián)查詢(xún)返回的是 yiidbActiveQuery 的實(shí)例,如果像特性(如類(lèi)屬性)那樣連接關(guān)聯(lián)數(shù)據(jù), 返回的結(jié)果是關(guān)聯(lián)查詢(xún)的結(jié)果,即 yiidbActiveRecord 的實(shí)例, 或者是數(shù)組,或者是 null ,取決于關(guān)聯(lián)關(guān)系的多樣性。
如,$customer->getOrders() 返回ActiveQuery 實(shí)例,而 $customer->orders 返回Order 對(duì)象數(shù)組 (如果查詢(xún)結(jié)果為空則返回空數(shù)組)。中間關(guān)聯(lián)表
有時(shí),兩個(gè)表通過(guò)中間表關(guān)聯(lián),定義這樣的關(guān)聯(lián)關(guān)系, 可以通過(guò)調(diào)用 yiidbActiveQuery::via() 方法或 yiidbActiveQuery::viaTable() 方法來(lái)定制 yiidbActiveQuery 對(duì)象 。
舉例而言,如果 order 表和 item 表通過(guò)中間表 order_item 關(guān)聯(lián)起來(lái), 可以在 Order 類(lèi)聲明 items 關(guān)聯(lián)關(guān)系取代中間表:
class Order extends yiidbActiveRecord{ public function getItems() { return $this->hasMany(Item::className(), ["id" => "item_id"]) ->viaTable("order_item", ["order_id" => "id"]); } }
兩個(gè)方法是相似的,除了 yiidbActiveQuery::via() 方法的第一個(gè)參數(shù)是使用 AR 類(lèi)中定義的關(guān)聯(lián)名。 以上方法取代了中間表,等價(jià)于:
class Order extends yiidbActiveRecord{ public function getOrderItems() { return $this->hasMany(OrderItem::className(), ["order_id" => "id"]); } public function getItems() { return $this->hasMany(Item::className(), ["id" => "item_id"]) ->via("orderItems"); } }延遲加載和即時(shí)加載(又稱(chēng)惰性加載與貪婪加載)
如前所述,當(dāng)你第一次連接關(guān)聯(lián)對(duì)象時(shí), AR 將執(zhí)行一個(gè)數(shù)據(jù)庫(kù)查詢(xún) 來(lái)檢索請(qǐng)求數(shù)據(jù)并填充到關(guān)聯(lián)對(duì)象的相應(yīng)屬性。如果再次連接相同的關(guān)聯(lián)對(duì)象,不再執(zhí)行任何查詢(xún)語(yǔ)句,這種數(shù)據(jù)庫(kù)查詢(xún)的執(zhí)行方法稱(chēng)為“延遲加載”。
如:
// SQL executed: SELECT * FROM customer WHERE id=1 $customer = Customer::findOne(1); // SQL executed: SELECT * FROM order WHERE customer_id=1 $orders = $customer->orders; // 沒(méi)有 SQL 語(yǔ)句被執(zhí)行 $orders2 = $customer->orders; //取回上次查詢(xún)的緩存數(shù)據(jù)
延遲加載非常實(shí)用,但是,在以下場(chǎng)景中使用延遲加載會(huì)遭遇性能問(wèn)題:
// SQL executed: SELECT * FROM customer LIMIT 100 $customers = Customer::find()->limit(100)->all(); foreach ($customers as $customer) { // SQL executed: SELECT * FROM order WHERE customer_id=... $orders = $customer->orders; // ...處理 $orders... }
假設(shè)數(shù)據(jù)庫(kù)查出的客戶(hù)超過(guò)100個(gè),以上代碼將執(zhí)行多少條 SQL 語(yǔ)句? 101 條!第一條 SQL 查詢(xún)語(yǔ)句取回100個(gè)客戶(hù),然后, 每個(gè)客戶(hù)要執(zhí)行一條 SQL 查詢(xún)語(yǔ)句以取回該客戶(hù)的所有訂單。
為解決以上性能問(wèn)題,可以通過(guò)調(diào)用 yiidbActiveQuery::with() 方法使用即時(shí)加載解決。
// SQL executed: SELECT * FROM customer LIMIT 100; // SELECT * FROM orders WHERE customer_id IN (1,2,...) $customers = Customer::find()->limit(100) ->with("orders")->all(); foreach ($customers as $customer) { // 沒(méi)有 SQL 語(yǔ)句被執(zhí)行 $orders = $customer->orders; // ...處理 $orders... }
如你所見(jiàn),同樣的任務(wù)只需要兩個(gè) SQL 語(yǔ)句。
須知:通常,即時(shí)加載 N 個(gè)關(guān)聯(lián)關(guān)系而通過(guò) via() 或者 viaTable() 定義了 M 個(gè)關(guān)聯(lián)關(guān)系, 將有 1+M+N 條 SQL 查詢(xún)語(yǔ)句被執(zhí)行:一個(gè)查詢(xún)?nèi)』刂鞅硇袛?shù), 一個(gè)查詢(xún)給每一個(gè) (M) 中間表,一個(gè)查詢(xún)給每個(gè) (N) 關(guān)聯(lián)表。
注意:當(dāng)用即時(shí)加載定制 select() 時(shí),確保連接 到關(guān)聯(lián)模型的列都被包括了,否則,關(guān)聯(lián)模型不會(huì)載入。如:
$orders = Order::find() ->select(["id", "amount"]) ->with("customer") ->all(); // $orders[0]->customer 總是空的,使用以下代碼解決這個(gè)問(wèn)題: $orders = Order::find() ->select(["id", "amount", "customer_id"]) ->with("customer") ->all();
有時(shí)候,你想自由的自定義關(guān)聯(lián)查詢(xún),延遲加載和即時(shí)加載都可以實(shí)現(xiàn),如:
$customer = Customer::findOne(1); // 延遲加載: SELECT * FROM order WHERE customer_id=1 AND subtotal>100 $orders = $customer->getOrders()->where("subtotal>100")->all(); // 即時(shí)加載: SELECT * FROM customer LIMIT 100 // SELECT * FROM order WHERE customer_id IN (1,2,...) AND subtotal>100 $customers = Customer::find() ->limit(100) ->with([ "orders" => function($query) { $query->andWhere("subtotal>100"); }, ]) ->all();逆關(guān)系
關(guān)聯(lián)關(guān)系通常成對(duì)定義,如:Customer 可以有個(gè)名為 orders 關(guān)聯(lián)項(xiàng), 而 Order 也有個(gè)名為customer 的關(guān)聯(lián)項(xiàng):
class Customer extends ActiveRecord{ .... public function getOrders() { return $this->hasMany(Order::className(), ["customer_id" => "id"]); } } class Order extends ActiveRecord{ .... public function getCustomer() { return $this->hasOne(Customer::className(), ["id" => "customer_id"]); } }
如果我們執(zhí)行以下查詢(xún),可以發(fā)現(xiàn)訂單的 customer 和 找到這些訂單的客戶(hù)對(duì)象并不是同一個(gè)。
連接 customer->orders 將觸發(fā)一條 SQL 語(yǔ)句 而連接一個(gè)訂單的 customer 將觸發(fā)另一條 SQL 語(yǔ)句。
// SELECT * FROM customer WHERE id=1 $customer = Customer::findOne(1); // 輸出 "不相同" // SELECT * FROM order WHERE customer_id=1 // SELECT * FROM customer WHERE id=1 if ($customer->orders[0]->customer === $customer) { echo "相同"; } else { echo "不相同"; }
為避免多余執(zhí)行的后一條語(yǔ)句,我們可以為 customer或 orders 關(guān)聯(lián)關(guān)系定義相反的關(guān)聯(lián)關(guān)系,通過(guò)調(diào)用 yiidbActiveQuery::inverseOf() 方法可以實(shí)現(xiàn)。
class Customer extends ActiveRecord{ .... public function getOrders() { return $this->hasMany(Order::className(), ["customer_id" => "id"]) ->inverseOf("customer"); } }
現(xiàn)在我們同樣執(zhí)行上面的查詢(xún),我們將得到:
// SELECT * FROM customer WHERE id=1 $customer = Customer::findOne(1); // 輸出相同 // SELECT * FROM order WHERE customer_id=1 if ($customer->orders[0]->customer === $customer) { echo "相同"; } else { echo "不相同"; }
以上我們展示了如何在延遲加載中使用相對(duì)關(guān)聯(lián)關(guān)系, 相對(duì)關(guān)系也可以用在即時(shí)加載中:
// SELECT * FROM customer // SELECT * FROM order WHERE customer_id IN (1, 2, ...) $customers = Customer::find()->with("orders")->all(); // 輸出相同 if ($customers[0]->orders[0]->customer === $customers[0]) { echo "相同"; } else { echo "不相同"; }
注意:相對(duì)關(guān)系不能在包含中間表的關(guān)聯(lián)關(guān)系中定義。
即是,如果你的關(guān)系是通過(guò)yiidbActiveQuery::via() 或 yiidbActiveQuery::viaTable()方法定義的, 就不能調(diào)用yiidbActiveQuery::inverseOf()方法了。JOIN 類(lèi)型關(guān)聯(lián)查詢(xún)
使用關(guān)系數(shù)據(jù)庫(kù)時(shí),普遍要做的是連接多個(gè)表并明確地運(yùn)用各種 JOIN 查詢(xún)。
JOIN SQL語(yǔ)句的查詢(xún)條件和參數(shù),使用 yiidbActiveQuery::joinWith() 可以重用已定義關(guān)系并調(diào)用 而不是使用 yiidbActiveQuery::join() 來(lái)實(shí)現(xiàn)目標(biāo)。
// 查找所有訂單并以客戶(hù) ID 和訂單 ID 排序,并貪婪加載 "customer" 表 $orders = Order::find() ->joinWith("customer") ->orderBy("customer.id, order.id") ->all(); // 查找包括書(shū)籍的所有訂單,并以 `INNER JOIN` 的連接方式即時(shí)加載 "books" 表 $orders = Order::find() ->innerJoinWith("books") ->all();
以上,方法 yiidbActiveQuery::innerJoinWith() 是訪問(wèn) INNER JOIN 類(lèi)型的 yiidbActiveQuery::joinWith() 的快捷方式。
可以連接一個(gè)或多個(gè)關(guān)聯(lián)關(guān)系,可以自由使用查詢(xún)條件到關(guān)聯(lián)查詢(xún), 也可以嵌套連接關(guān)聯(lián)查詢(xún)。如:
// 連接多重關(guān)系 // 找出24小時(shí)內(nèi)注冊(cè)客戶(hù)包含書(shū)籍的訂單 $orders = Order::find() ->innerJoinWith([ "books", "customer" => function ($query) { $query->where("customer.created_at > " . (time() - 24 * 3600)); } ]) ->all(); // 連接嵌套關(guān)系:連接 books 表及其 author 列 $orders = Order::find() ->joinWith("books.author") ->all();
代碼背后, Yii 先執(zhí)行一條 JOIN SQL 語(yǔ)句把滿(mǎn)足 JOIN SQL 語(yǔ)句查詢(xún)條件的主要模型查出, 然后為每個(gè)關(guān)系執(zhí)行一條查詢(xún)語(yǔ)句, bing填充相應(yīng)的關(guān)聯(lián)記錄。
yiidbActiveQuery::joinWith() 和 yiidbActiveQuery::with() 的區(qū)別是: 前者連接主模型類(lèi)和關(guān)聯(lián)模型類(lèi)的數(shù)據(jù)表來(lái)檢索主模型, 而后者只查詢(xún)和檢索主模型類(lèi)。 檢索主模型由于這個(gè)區(qū)別,你可以應(yīng)用只針對(duì)一條 JOIN SQL 語(yǔ)句起效的查詢(xún)條件。 如,通過(guò)關(guān)聯(lián)模型的查詢(xún)條件過(guò)濾主模型,如前例, 可以使用關(guān)聯(lián)表的列來(lái)挑選主模型數(shù)據(jù), 當(dāng)使用 yiidbActiveQuery::joinWith() 方法時(shí)可以響應(yīng)沒(méi)有歧義的列名。 當(dāng)連接關(guān)聯(lián)關(guān)系時(shí),關(guān)聯(lián)關(guān)系默認(rèn)使用即時(shí)加載。 你可以 通過(guò)傳參數(shù) $eagerLoading 來(lái)決定在指定關(guān)聯(lián)查詢(xún)中是否使用即時(shí)加載。 默認(rèn) yiidbActiveQuery::joinWith() 使用左連接來(lái)連接關(guān)聯(lián)表。 你也可以傳 $joinType 參數(shù)來(lái)定制連接類(lèi)型。 你也可以使用 yiidbActiveQuery::innerJoinWith()。
以下是 INNER JOIN 的簡(jiǎn)短例子:
// 查找包括書(shū)籍的所有訂單,但 "books" 表不使用即時(shí)加載 $orders = Order::find() ->innerJoinWith("books", false) ->all(); // 等價(jià)于: $orders = Order::find() ->joinWith("books", false, "INNER JOIN") ->all();
有時(shí)連接兩個(gè)表時(shí),需要在關(guān)聯(lián)查詢(xún)的 ON 部分指定額外條件。
這可以通過(guò)調(diào)用 yiidbActiveQuery::onCondition() 方法實(shí)現(xiàn):
class User extends ActiveRecord{ public function getBooks() { return $this->hasMany(Item::className(), ["owner_id" => "id"]) ->onCondition(["category_id" => 1]); } }
在上面, yiidbActiveRecord::hasMany() 方法回傳了一個(gè) yiidbActiveQuery 對(duì)象, 當(dāng)你用 yiidbActiveQuery::joinWith() 執(zhí)行一條查詢(xún)時(shí),取決于正被調(diào)用的是哪個(gè) yiidbActiveQuery::onCondition(), 返回 category_id 為 1 的 items。
當(dāng)你用 yiidbActiveQuery::joinWith() 進(jìn)行一次查詢(xún)時(shí),“on-condition”條件會(huì)被放置在相應(yīng)查詢(xún)語(yǔ)句的 ON 部分, 如:
// SELECT user.* FROM user LEFT JOIN item ON item.owner_id=user.id AND category_id=1 // SELECT * FROM item WHERE owner_id IN (...) AND category_id=1 $users = User::find()->joinWith("books")->all();
注意:如果通過(guò) yiidbActiveQuery::with() 進(jìn)行貪婪加載或使用惰性加載的話(huà),則 on 條件會(huì)被放置在對(duì)應(yīng) SQL語(yǔ)句的 WHERE 部分。 因?yàn)?,此時(shí)此處并沒(méi)有發(fā)生 JOIN 查詢(xún)。比如:
// SELECT * FROM user WHERE id=10 $user = User::findOne(10); // SELECT * FROM item WHERE owner_id=10 AND category_id=1 $books = $user->books;關(guān)聯(lián)表操作
AR 提供了下面兩個(gè)方法用來(lái)建立和解除兩個(gè)關(guān)聯(lián)對(duì)象之間的關(guān)系:
yiidbActiveRecord::link()
yiidbActiveRecord::unlink()
例如,給定一個(gè)customer和order對(duì)象,我們可以通過(guò)下面的代碼使得customer對(duì)象擁有order對(duì)象:
$customer = Customer::findOne(1); $order = new Order(); $order->subtotal = 100; $customer->link("orders", $order);
作用域yiidbActiveRecord::link() 調(diào)用上述將設(shè)置 customer_id 的順序是 $customer 的主鍵值,然后調(diào)用 yiidbActiveRecord::save() 要將順序保存到數(shù)據(jù)庫(kù)中。
當(dāng)你調(diào)用yiidbActiveRecord::find() 或 yiidbActiveRecord::findBySql()方法時(shí),將會(huì)返回一個(gè)yiidbActiveQuery實(shí)例。
之后,你可以調(diào)用其他查詢(xún)方法,如 yiidbActiveQuery::where(),yiidbActiveQuery::orderBy(), 進(jìn)一步的指定查詢(xún)條件。
有時(shí)候你可能需要在不同的地方使用相同的查詢(xún)方法。如果出現(xiàn)這種情況,你應(yīng)該考慮定義所謂的作用域。
作用域是本質(zhì)上要求一組的查詢(xún)方法來(lái)修改查詢(xún)對(duì)象的自定義查詢(xún)類(lèi)中定義的方法。
之后你就可以像使用普通方法一樣使用作用域。只需兩步即可定義一個(gè)作用域。
首先給你的model創(chuàng)建一個(gè)自定義的查詢(xún)類(lèi),在此類(lèi)中定義的所需的范圍方法。
例如,給Comment模型創(chuàng)建一個(gè) CommentQuery類(lèi),然后在CommentQuery類(lèi)中定義一個(gè)active()的方法為作用域,像下面的代碼:
namespace appmodels; use yiidbActiveQuery; class CommentQuery extends ActiveQuery{ public function active($state = true) { $this->andWhere(["active" => $state]); return $this; } }
重點(diǎn):
類(lèi)必須繼承 yiidbActiveQuery (或者是其他的 ActiveQuery ,比如 yiimongodbActiveQuery)。
必須是一個(gè)public類(lèi)型的方法且必須返回 $this 實(shí)現(xiàn)鏈?zhǔn)讲僮???梢詡魅雲(yún)?shù)。
檢查 yiidbActiveQuery 對(duì)于修改查詢(xún)條件是非常有用的方法。
其次,覆蓋yiidbActiveRecord::find() 方法使其返回自定義的查詢(xún)對(duì)象而不是常規(guī)的yiidbActiveQuery。
對(duì)于上述例子,你需要編寫(xiě)如下代碼:
namespace appmodels; use yiidbActiveRecord; class Comment extends ActiveRecord{ /** * @inheritdoc * @return CommentQuery */ public static function find() { return new CommentQuery(get_called_class()); } }
就這樣,現(xiàn)在你可以使用自定義的作用域方法了:
$comments = Comment::find() ->active() ->all(); $inactiveComments = Comment::find() ->active(false) ->all();
你也能在定義的關(guān)聯(lián)里使用作用域方法,比如:
class Post extends yiidbActiveRecord{ public function getActiveComments() { return $this->hasMany(Comment::className(), ["post_id" => "id"]) ->active(); } }
或者在執(zhí)行關(guān)聯(lián)查詢(xún)的時(shí)候使用(on-the-fly 是啥?):
$posts = Post::find() ->with([ "comments" => function($q) { $q->active(); } ]) ->all();默認(rèn)作用域
如果你之前用過(guò) Yii 1.1 就應(yīng)該知道默認(rèn)作用域的概念。
一個(gè)默認(rèn)的作用域可以作用于所有查詢(xún)。
你可以很容易的通過(guò)重寫(xiě)yiidbActiveRecord::find()方法來(lái)定義一個(gè)默認(rèn)作用域,例如:
public static function find(){ return parent::find() ->where(["deleted" => false]); }
注意,你之后所有的查詢(xún)都不能用 yiidbActiveQuery::where(),但是可以用 yiidbActiveQuery::andWhere() 和 yiidbActiveQuery::orWhere(),他們不會(huì)覆蓋掉默認(rèn)作用域。 (譯注:如果你要使用默認(rèn)作用域,就不能在 xxx::find()后使用where()方法,你必須使用andXXX()或者orXXX()系的方法,否則默認(rèn)作用域不會(huì)起效果,至于原因,打開(kāi)where()方法的代碼一看便知)事務(wù)操作
當(dāng)執(zhí)行幾個(gè)相關(guān)聯(lián)的數(shù)據(jù)庫(kù)操作的時(shí)候
TODO: FIXME: WIP, TBD, https://github.com/yiisoft/yii2/issues/226 , yiidbActiveRecord::afterSave(), yiidbActiveRecord::beforeDelete() and/or yiidbActiveRecord::afterDelete()
生命周期周期方法(life cycle methods 我覺(jué)得這句翻譯成“模板方法”會(huì)不會(huì)更好點(diǎn)?)。
開(kāi)發(fā)者可以通過(guò)重寫(xiě)yiidbActiveRecord::save()方法然后在控制器里使用事務(wù)操作,嚴(yán)格地說(shuō)是似乎不是一個(gè)好的做法 (召回"瘦控制器 / 肥模型"基本規(guī)則)。
這些方法在這里(如果你不明白自己實(shí)際在干什么,請(qǐng)不要使用他們),Models:
class Feature extends yiidbActiveRecord{ // ... public function getProduct() { return $this->hasOne(Product::className(), ["id" => "product_id"]); } } class Product extends yiidbActiveRecord{ // ... public function getFeatures() { return $this->hasMany(Feature::className(), ["product_id" => "id"]); } }
重寫(xiě) yiidbActiveRecord::save() 方法:
class ProductController extends yiiwebController{ public function actionCreate() { // FIXME: TODO: WIP, TBD } }
在控制器層使用事務(wù):
class ProductController extends yiiwebController{ public function actionCreate() { // FIXME: TODO: WIP, TBD } }
作為這些脆弱方法的替代,你應(yīng)該使用原子操作方案特性。
class Feature extends yiidbActiveRecord{ // ... public function getProduct() { return $this->hasOne(Product::className(), ["product_id" => "id"]); } public function scenarios() { return [ "userCreates" => [ "attributes" => ["name", "value"], "atomic" => [self::OP_INSERT], ], ]; } } class Product extends yiidbActiveRecord{ // ... public function getFeatures() { return $this->hasMany(Feature::className(), ["id" => "product_id"]); } public function scenarios() { return [ "userCreates" => [ "attributes" => ["title", "price"], "atomic" => [self::OP_INSERT], ], ]; } public function afterValidate() { parent::afterValidate(); // FIXME: TODO: WIP, TBD } public function afterSave($insert) { parent::afterSave($insert); if ($this->getScenario() === "userCreates") { // FIXME: TODO: WIP, TBD } } }
Controller里的代碼將變得很簡(jiǎn)潔:
class ProductController extends yiiwebController{ public function actionCreate() { // FIXME: TODO: WIP, TBD } }
控制器非常簡(jiǎn)潔:
class ProductController extends yiiwebController{ public function actionCreate() { // FIXME: TODO: WIP, TBD } }被污染屬性
當(dāng)你調(diào)用yiidbActiveRecord::save()用于保存活動(dòng)記錄(Active Record)實(shí)例時(shí),只有被污染的屬性才會(huì)被保存。
一個(gè)屬性是否認(rèn)定為被污染取決于它的值自從最后一次從數(shù)據(jù)庫(kù)加載或者最近一次保存到數(shù)據(jù)庫(kù)后到現(xiàn)在是否被修改過(guò)。注意:無(wú)論活動(dòng)記錄(Active Record)是否有被污染屬性,數(shù)據(jù)驗(yàn)證始終會(huì)執(zhí)行。
活動(dòng)記錄(Active Record)會(huì)自動(dòng)維護(hù)一個(gè)污染數(shù)據(jù)列表。它的工作方式是通過(guò)維護(hù)一個(gè)較舊屬性值版本,并且將它們與最新的進(jìn)行比較。
你可以通過(guò)調(diào)用yiidbActiveRecord::getDirtyAttributes()來(lái)獲取當(dāng)前的污染屬性。
你也可以調(diào)用yiidbActiveRecord::markAttributeDirty()來(lái)顯示的標(biāo)記一個(gè)屬性為污染屬性。
如果你對(duì)最近一次修改前的屬性值感興趣,你可以調(diào)用yiidbActiveRecord::getOldAttributes() 或 yiidbActiveRecord::getOldAttribute()。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/21650.html
摘要:運(yùn)行來(lái)安裝指定的擴(kuò)展。這更便于用戶(hù)辨別是否是的擴(kuò)展。當(dāng)用戶(hù)運(yùn)行安裝一個(gè)擴(kuò)展時(shí),文件會(huì)被自動(dòng)更新使之包含新擴(kuò)展的信息。上述代碼表明該擴(kuò)展依賴(lài)于包。例如,上述的條目聲明將對(duì)應(yīng)于別名。為達(dá)到這個(gè)目的,你應(yīng)當(dāng)在公開(kāi)發(fā)布前做測(cè)試。 簡(jiǎn)述 擴(kuò)展是專(zhuān)門(mén)設(shè)計(jì)的在 Yii 應(yīng)用中隨時(shí)可拿來(lái)使用的, 并可重發(fā)布的軟件包。 基礎(chǔ) 例如, yiisoft/yii2-debug 擴(kuò)展在你的應(yīng)用的每個(gè)頁(yè)面底部添加...
摘要:認(rèn)證事件類(lèi)在登錄和注銷(xiāo)流程引發(fā)一些事件。成功注銷(xiāo)后引發(fā)。提供兩種授權(quán)方法存取控制過(guò)濾器和基于角色的存取控制。允許已認(rèn)證用戶(hù)執(zhí)行操作。指定一個(gè)回調(diào)函數(shù)用于判定該規(guī)則是否滿(mǎn)足條件。 簡(jiǎn)述 在程序開(kāi)發(fā)過(guò)程中,往往都不能忽視安全問(wèn)題,無(wú)論你的框架有多么完美,都會(huì)有破綻,所以完善自己的系統(tǒng),從程序開(kāi)發(fā)的安全角度去思考問(wèn)題,把一切潛在的危機(jī)扼殺在搖籃中。 認(rèn)證(Authentication) 認(rèn)證...
摘要:簡(jiǎn)述這里簡(jiǎn)單歸納總結(jié)關(guān)于的錯(cuò)誤處理和日志記錄的操作。錯(cuò)誤處理器會(huì)正確地設(shè)置響應(yīng)的狀態(tài)碼并使用合適的錯(cuò)誤視圖頁(yè)面來(lái)顯示錯(cuò)誤信息。記錄一個(gè)警告消息用來(lái)指示一些已經(jīng)發(fā)生的意外。的義務(wù)是正確處理日志消息。相應(yīng)的消息通過(guò)被記錄。 簡(jiǎn)述 這里簡(jiǎn)單歸納總結(jié)關(guān)于Yii的錯(cuò)誤處理和日志記錄的操作。 錯(cuò)誤處理(Errors) Yii 內(nèi)置了一個(gè)yiiwebErrorHandler錯(cuò)誤處理器,它使錯(cuò)誤處理更...
摘要:把所有的增量數(shù)據(jù)庫(kù)遷移提交到生產(chǎn)環(huán)境數(shù)據(jù)庫(kù)當(dāng)中。如果其中任意一個(gè)遷移提交失敗了,那么這條命令將會(huì)退出并停止剩下的那些還未執(zhí)行的遷移。執(zhí)行這條命令期間不會(huì)有任何的遷移會(huì)被提交或還原。 簡(jiǎn)述 數(shù)據(jù)遷移就是數(shù)據(jù)庫(kù)表在團(tuán)隊(duì)建的遷移操作,達(dá)到團(tuán)隊(duì)相互間的信息同步,數(shù)據(jù)統(tǒng)一。 數(shù)據(jù)庫(kù)遷移 一般步驟: 1、在 yii2 的 migrate 中,通常用來(lái)對(duì)數(shù)據(jù)庫(kù)數(shù)據(jù)表進(jìn)行修改操作,主要對(duì)結(jié)構(gòu)和小部分?jǐn)?shù)...
摘要:它由一個(gè)或多個(gè)類(lèi)組成,它們?cè)诳刂婆_(tái)環(huán)境下通常被稱(chēng)為命令。控制臺(tái)入口腳本通常被稱(chēng)為,位于應(yīng)用程序的根目錄。選項(xiàng)通過(guò)覆蓋在中的方法,你可以指定可用于控制臺(tái)命令選項(xiàng)。參數(shù)將傳遞給請(qǐng)求的子命令對(duì)應(yīng)的操作方法。通常,執(zhí)行成功的命令會(huì)返回。 簡(jiǎn)述 控制臺(tái)應(yīng)用程序的結(jié)構(gòu)非常類(lèi)似于 Yii 的一個(gè) Web 應(yīng)用程序,主要用于終端服務(wù)器執(zhí)行。 控制臺(tái)命令 控制臺(tái)應(yīng)用程序的結(jié)構(gòu)非常類(lèi)似于 Yii 的一個(gè) ...
閱讀 709·2023-04-25 18:59
閱讀 1252·2021-09-22 16:00
閱讀 1915·2021-09-22 15:42
閱讀 3630·2021-09-22 15:27
閱讀 1274·2019-08-30 15:54
閱讀 1136·2019-08-30 11:16
閱讀 2476·2019-08-29 16:24
閱讀 855·2019-08-29 12:14