diff --git a/docs/api/db/ActiveRecord-find.md b/docs/api/db/ActiveRecord-find.md index 39ab80f..570c423 100644 --- a/docs/api/db/ActiveRecord-find.md +++ b/docs/api/db/ActiveRecord-find.md @@ -1,5 +1,5 @@ -Because [[ActiveQuery]] implements a set of query building methods, -additional query conditions can be specified by calling the methods of [[ActiveQuery]]. +The returned [[ActiveQuery]] instance can be further customized by calling +methods defined in [[ActiveQuery]] before returning the populated active records. Below are some examples: diff --git a/docs/api/db/ActiveRecord.md b/docs/api/db/ActiveRecord.md index 8b13789..27eeb31 100644 --- a/docs/api/db/ActiveRecord.md +++ b/docs/api/db/ActiveRecord.md @@ -1 +1,353 @@ +ActiveRecord implements the [Active Record design pattern](http://en.wikipedia.org/wiki/Active_record). +An ActiveRecord object is associated with a row in a database table. For example, a `Customer` object +is associated with a row in the `tbl_customer` table. Instead of writing raw SQL statements to access +the data in the table, one can call intuitive methods available in the corresponding ActiveRecord class +to achieve the same goals. For example, calling [[save()]] would insert or update a row +in the underlying table. + +### Declaring ActiveRecord Classes + +An ActiveRecord class is declared by extending [[\yii\db\ActiveRecord]]. It typically requires the following +minimal code: + +~~~ +class Customer extends \yii\db\ActiveRecord +{ + /** + * @return string the name of the table associated with this ActiveRecord class. + */ + public static function tableName() + { + return 'tbl_customer'; + } +} +~~~ + + +### Connecting to Database + +ActiveRecord relies on a [[Connection|DB connection]] to perform DB-related operations. By default, +it assumes that an application component named `db` gives the needed [[Connection]] instance +which serves as the DB connection. The following application configuration shows an example: + +~~~ +return array( + 'components' => array( + 'db' => array( + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=testdb', + 'username' => 'demo', + 'password' => 'demo', + // turn on schema caching to improve performance + // 'schemaCacheDuration' => 3600, + ), + ), +); +~~~ + + +### Retrieving Data from Database + +ActiveRecord provides three methods for data retrieval purpose: + +- [[find()]] +- [[findBySql()]] +- [[count()]] + +They all return an [[ActiveQuery]] instance. Coupled with the various customization and query methods +provided by [[ActiveQuery]], ActiveRecord supports very flexible and powerful data retrieval approaches. + +The followings are some examples, + +~~~ +// to retrieve all *active* customers and order them by their ID: +$customers = Customer::find() + ->where(array('status' => $active)) + ->orderBy('id') + ->all(); + +// to return a single customer whose ID is 1: +$customer = Customer::find() + ->where(array('id' => 1)) + ->one(); + +// or use the following shortcut approach: +$customer = Customer::find(1); + +// to retrieve customers using a raw SQL statement: +$sql = 'SELECT * FROM tbl_customer'; +$customers = Customer::findBySql($sql)->all(); + +// to return the number of *active* customers: +$count = Customer::count() + ->where(array('status' => $active)) + ->value(); + +// to return customers in terms of arrays rather than `Customer` objects: +$customers = Customer::find()->asArray()->all(); +// each $customers element is an array of name-value pairs + +// to index the result by customer IDs: +$customers = Customer::find()->indexBy('id')->all(); +// $customers array is indexed by customer IDs +~~~ + + +### Accessing Column Data + +ActiveRecord maps each column in the associated row of database table to an *attribute* in the ActiveRecord +object. An attribute is like a regular object property whose name is the same as the corresponding column +name and is case sensitive. + +To read the value of a column, we can use the following expression: + +~~~ +// "id" is the name of a column in the table associated with $customer ActiveRecord object +$id = $customer->id; +// or alternatively, +$id = $customer->getAttribute('id'); +~~~ + +And through the [[attributes]] property, we can get all column values: + +~~~ +$values = $customer->attributes; +~~~ + + +### Persisting Data to Database + +ActiveRecord provides the following methods to support data insertion, updating and deletion: + +- [[save()]] +- [[insert()]] +- [[update()]] +- [[delete()]] +- [[updateCounters()]] +- [[updateAll()]] +- [[updateAllCounters()]] +- [[deleteAll()]] + +Note that [[updateAll()]], [[updateAllCounters()]] and [[deleteAll()]] apply to the whole database +table, while the rest of the methods only apply to the row associated with the ActiveRecord object. + +The followings are some examples: + +~~~ +// to insert a new customer record +$customer = new Customer; +$customer->name = 'James'; +$customer->email = 'james@example.com'; +$customer->save(); // equivalent to $customer->insert(); + +// to update an existing customer record +$customer = Customer::find($id); +$customer->email = 'james@example.com'; +$customer->save(); // equivalent to $customer->update(); + +// to delete an existing customer record +$customer = Customer::find($id); +$customer->delete(); + +// to increment the age of all customers by 1 +Customer::updateAllCounters(array('age' => 1)); +~~~ + + +### Retrieving Relational Data + +ActiveRecord supports foreign key relationships by exposing them via component properties. For example, +with appropriate declaration, the expression `$customer->orders` can return an array of `Order` objects +which represent the orders placed by the specified customer. + +To declare a relationship, define a getter method which returns an [[ActiveRelation]] object. For example, + +~~~ +class Customer extends \yii\db\ActiveRecord +{ + public function getOrders() + { + return $this->hasMany('Order', array('customer_id' => 'id')); + } +} + +class Order extends \yii\db\ActiveRecord +{ + public function getCustomer() + { + return $this->hasOne('Customer', array('id' => 'customer_id')); + } +} +~~~ + +Within the getter methods, we call [[hasMany()]] or [[hasOne()]] to create a new [[ActiveRelation]] object. +The [[hasMany()]] method declares a one-many relationship. For example, a customer has many orders. +And the [[hasOne()]] method declares a many-one or one-one relationship. For example, an order has one customer. +Both methods take two parameters: + +- `$class`: the class name of the related models. If the class name is not namespaced, it will take + the same namespace as the declaring class. +- `$link`: the association between columns from two tables. This should be given as an array. + The keys of the array are the names of the columns from the table associated with `$class`, + while the values of the array the names of the columns from the declaring class. + +Retrieving relational data is now as easy as accessing a component property. Remember that a component +property is defined by the existence of a getter method. The The following example +shows how to get the orders of a customer, and how to get the customer of the first order. + +~~~ +$customer = Customer::find($id); +$orders = $customer->orders; // $orders is an array of Order objects +$customer2 = $orders[0]->customer; // $customer == $customer2 +~~~ + +Because [[ActiveRelation]] extends from [[ActiveQuery]], it has the same query customization methods, +which allows us to customize the query for retrieving the related objects. For example, we may declare a `bigOrder` +relationship which returns orders whose subtotal exceeds certain amount: + +~~~ +class Customer extends \yii\db\ActiveRecord +{ + public function getBigOrders() + { + return $this->hasMany('Order', array('customer_id' => 'id')) + ->where('subtotal > 100') + ->orderBy('id'); + } +} +~~~ + + +Sometimes, two tables are related together via an intermediary table called +[pivot table](http://en.wikipedia.org/wiki/Pivot_table). To declare such relationships, we can customize +the [[ActiveRelation]] object by calling its [[ActiveRelation::via()]] or [[ActiveRelation::viaTable()]] +method. + +For example, if table `tbl_order` and table `tbl_item` are related via pivot table `tbl_order_item`, +we can declare the `items` relation in the `Order` class like the following: + +~~~ +class Order extends \yii\db\ActiveRecord +{ + public function getItems() + { + return $this->hasMany('Item', array('id' => 'item_id')) + ->viaTable('tbl_order_item', array('order_id' => 'id')); + } +} +~~~ + +Method [[ActiveRelation::via()]] is similar to [[ActiveRelation::viaTable()]] except that +the first parameter of [[ActiveRelation::via()]] takes a relation name declared in the ActiveRecord class. +For example, the above `items` relation can be equivalently declared as follows: + +~~~ +class Order extends \yii\db\ActiveRecord +{ + public function getOrderItems() + { + return $this->hasMany('OrderItem', array('order_id' => 'id')); + } + + public function getItems() + { + return $this->hasMany('Item', array('id' => 'item_id')) + ->via('orderItems'); + } +} +~~~ + + +When we access the related objects the first time, behind the scene ActiveRecord will perform a DB query +to retrieve the corresponding data and populate them into the related objects. No query will be perform +if we access again the same related objects. We call this *lazy loading*. For example, + +~~~ +// SQL executed: SELECT * FROM tbl_customer WHERE id=1 +$customer = Customer::find(1); +// SQL executed: SELECT * FROM tbl_order WHERE customer_id=1 +$orders = $customer->orders; +// no SQL executed +$orders2 = $customer->orders; +~~~ + + +Lazy loading is convenient to use. However, it may suffer from performance issue in the following scenario: + +~~~ +// SQL executed: SELECT * FROM tbl_customer LIMIT 100 +$customers = Customer::find()->limit(100)->all(); + +foreach ($customers as $customer) { + // SQL executed: SELECT * FROM tbl_order WHERE customer_id=... + $orders = $customer->orders; + // ...handle $orders... +} +~~~ + +How many SQL queries will be performed in the above code, assuming there are more than 100 customers in +the database? 101! The first SQL query brings back 100 customers. Then for each customer, a SQL query +is performed to bring back the customer's orders. + +To solve the above performance problem, we can use the so-called *eager loading* by calling [[ActiveQuery::with()]]: + +~~~ +// SQL executed: SELECT * FROM tbl_customer LIMIT 100 +// SELECT * FROM tbl_orders WHERE customer_id IN (1,2,...) +$customers = Customer::find()->limit(100) + ->with('orders')->all(); + +foreach ($customers as $customer) { + // no SQL executed + $orders = $customer->orders; + // ...handle $orders... +} +~~~ + +As we can see, only two SQL queries are needed for the same task. + + +Sometimes, we may want to customize the relational queries on the fly. This can be done for both +lazy loading and eager loading. For example, + +~~~ +$customer = Customer::find(1); +// lazy loading: SELECT * FROM tbl_order WHERE customer_id=1 AND subtotal>100 +$orders = $customer->getOrders()->where('subtotal>100')->all(); + +// eager loading: SELECT * FROM tbl_customer LIMIT 10 + SELECT * FROM tbl_order WHERE customer_id IN (1,2,...) AND subtotal>100 +$customers = Customer::find()->limit(100)->with(array( + 'orders' => function($query) { + $query->andWhere('subtotal>100'); + }, +))->all(); +~~~ + + +### Maintaining Relationships + +ActiveRecord provides the following two methods for establishing and breaking relationship +between two ActiveRecord objects: + +- [[link()]] +- [[unlink()]] + +For example, given a customer and a new order, we can use the following code to make the +order owned by the customer: + +~~~ +$customer = Customer::find(1); +$order = new Order; +$order->subtotal = 100; +$customer->link('orders', $order); +~~~ + +The [[link()]] call above will set the `customer_id` of the order to be the primary key +value of `$customer` and then call [[save()]] to save the order into database. + + +### Data Input and Validation + +// todo \ No newline at end of file diff --git a/framework/db/ActiveQuery.php b/framework/db/ActiveQuery.php index d47ec18..90e508f 100644 --- a/framework/db/ActiveQuery.php +++ b/framework/db/ActiveQuery.php @@ -37,7 +37,6 @@ use yii\db\Exception; * - [[with]]: list of relations that this query should be performed with. * - [[indexBy]]: the name of the column by which the query result should be indexed. * - [[asArray]]: whether to return each record as an array. - * - [[scopes]]: list of scopes that should be applied to this query. * * These options can be configured using methods of the same name. For example: * @@ -69,32 +68,11 @@ class ActiveQuery extends Query */ public $asArray; /** - * @var array list of scopes that should be applied to this query - */ - public $scopes; - /** * @var string the SQL statement to be executed for retrieving AR records. * This is set by [[ActiveRecord::findBySql()]]. */ public $sql; - /** - * PHP magic method. - * This method is overridden so that scope methods declared in [[modelClass]] - * can be invoked as methods of ActiveQuery. - * @param string $name - * @param array $params - * @return mixed|ActiveQuery - */ - public function __call($name, $params) - { - if (method_exists($this->modelClass, $name)) { - $this->scopes[$name] = $params; - return $this; - } else { - return parent::__call($name, $params); - } - } /** * Executes query and returns all results as an array. @@ -179,9 +157,6 @@ class ActiveQuery extends Query $tableName = $modelClass::tableName(); $this->from = array($tableName); } - if (!empty($this->scopes)) { - $this->applyScopes($this->scopes); - } /** @var $qb QueryBuilder */ $qb = $db->getQueryBuilder(); $this->sql = $qb->build($this); @@ -243,37 +218,6 @@ class ActiveQuery extends Query return $this; } - /** - * Specifies the scopes to be applied to this query. - * - * The parameters to this method can be either one or multiple strings, or a single array - * of scopes names and their corresponding customization parameters. - * - * The followings are some usage examples: - * - * ~~~ - * // find all active customers - * Customer::find()->scopes('active')->all(); - * // find active customers whose age is greater than 30 - * Customer::find()->scopes(array( - * 'active', - * 'olderThan' => array(30), - * ))->all(); - * // alternatively the above statement can be written as: - * Customer::find()->active()->olderThan(30)->all(); - * ~~~ - * @return ActiveQuery the query object itself - */ - public function scopes() - { - $this->scopes = func_get_args(); - if (isset($this->scopes[0]) && is_array($this->scopes[0])) { - // the parameter is given as an array - $this->scopes = $this->scopes[0]; - } - return $this; - } - private function createModels($rows) { $models = array(); @@ -352,17 +296,4 @@ class ActiveQuery extends Query } return $relations; } - - private function applyScopes($scopes) - { - $modelClass = $this->modelClass; - foreach ($scopes as $name => $config) { - if (is_integer($name)) { - $modelClass::$config($this); - } else { - array_unshift($config, $this); - call_user_func_array(array($modelClass, $name), $config); - } - } - } } diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index 90d817d..860a388 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -20,7 +20,7 @@ use yii\db\Expression; use yii\util\StringHelper; /** - * ActiveRecord is the base class for classes representing relational data. + * ActiveRecord is the base class for classes representing relational data in terms of objects. * * @include @yii/db/ActiveRecord.md * @@ -1127,9 +1127,10 @@ abstract class ActiveRecord extends Model * @param ActiveRecord $model the model to be unlinked from the current one. * @param boolean $delete whether to delete the model that contains the foreign key. * If false, the model's foreign key will be set null and saved. + * If true, the model containing the foreign key will be deleted. * @throws BadParamException if the models cannot be unlinked */ - public function unlink($name, $model, $delete = true) + public function unlink($name, $model, $delete = false) { $relation = $this->getRelation($name); diff --git a/tests/unit/data/ar/Customer.php b/tests/unit/data/ar/Customer.php index d242f4d..a04694a 100644 --- a/tests/unit/data/ar/Customer.php +++ b/tests/unit/data/ar/Customer.php @@ -19,13 +19,4 @@ class Customer extends ActiveRecord { return $this->hasMany('Order', array('customer_id' => 'id'))->orderBy('id'); } - - /** - * @param ActiveQuery $query - * @return ActiveQuery - */ - public static function active($query) - { - return $query->andWhere(array('status' => self::STATUS_ACTIVE)); - } } \ No newline at end of file diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index 6cf0ea2..a0809b1 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -95,15 +95,6 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $this->assertEquals('user2', $customer->name); } - public function testScope() - { - $customers = Customer::find()->scopes('active')->all(); - $this->assertEquals(2, count($customers)); - - $customers = Customer::find()->active()->all(); - $this->assertEquals(2, count($customers)); - } - public function testFindLazy() { /** @var $customer Customer */ @@ -256,7 +247,7 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase // has many $customer = Customer::find(2); $this->assertEquals(2, count($customer->orders)); - $customer->unlink('orders', $customer->orders[1]); + $customer->unlink('orders', $customer->orders[1], true); $this->assertEquals(1, count($customer->orders)); $this->assertNull(Order::find(3)); @@ -264,14 +255,14 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $order = Order::find(2); $this->assertEquals(3, count($order->items)); $this->assertEquals(3, count($order->orderItems)); - $order->unlink('items', $order->items[2]); + $order->unlink('items', $order->items[2], true); $this->assertEquals(2, count($order->items)); $this->assertEquals(2, count($order->orderItems)); // via table $order = Order::find(1); $this->assertEquals(2, count($order->books)); - $order->unlink('books', $order->books[1]); + $order->unlink('books', $order->books[1], true); $this->assertEquals(1, count($order->books)); $this->assertEquals(1, count($order->orderItems)); }