yiisoft / active-record Goto Github PK
View Code? Open in Web Editor NEWActive Record database abstraction layer
Home Page: https://www.yiiframework.com/
License: BSD 3-Clause "New" or "Revised" License
Active Record database abstraction layer
Home Page: https://www.yiiframework.com/
License: BSD 3-Clause "New" or "Revised" License
Currently relationship definitions in AR are defined as non static getters. In reality an AR class represents a table and an instance of that class represents a record.
Relations are defined at the table level, but because AR uses non-static getters our query classes need to instantiate a dummy model for greedy queries.
Requirements for AR relationships:
Pros of current approach:
Cons:
In Yii1 we had a single function defining the relationships but we also had the dummy model due to lack of late static binding. This allowed for enumeration but had other down sides.
What if, in Yii3 we try to get the best of both? Suppose we define relationships as static functions:
public static function relatedCustomers()
{
return Has::many (Customer::class, ['id' => 'customer_id']);
}
Internally we would have a sound definition of the relationship, from this we can:
From a consumer point of view nothing changes:
getCustomers
via __invoke()
->customers
via __get()
Pros:
Cons:
The behavior of link()
doesn't feel intuitive and in some cases even faulty to me.
Consider I have models A, B and C.
Table for model A looks like this:
id (pk)
b_id (int) --> foreign key constraint on B
c_id (int) --> foreign key constraint on C
f1 (varchar)
f2 (int)
Now I'm creating a new model A
:
$b = ..;
$c = ..;
$a = new A();
$a->load(...);
$a->link('relationB', $b);
$a->link('relationC', $c);
$a->save();
The reason I want to use link is that my model defines how the relationship works, code using the model doesn't care or know about field names, it just knows about relation names.
The code above will not work, since the first call to link
will cause an exception because of the foreign key constraint on c_id
.
The final call to $a->save()
doesn't really do anything since link
already takes care of saving.
I'm not sure what the best approach forward is, but currently using link()
requires a very detailed knowledge of the underlying models that kind of makes the function useless in scenarios where I can just set the foreign key manually on the newly created object.
One possible simplification would be that link()
never saves new records.
Consider: $a->link('relationB', $b)
.
The behavior would then be:
$b->isNewRecord() ? exception
$a->isNewRecord() ? update field b_id, no database write.
!$a->isNewRecord() ? update field b_id, only save field b_id.
// Foreign key a_id in b instead.
$a->isNewRecord() ? exception
$b->isNewRecord() ? update field a_id, no database write.
!$a->isNewRecord() ? update field a_id, only save field a_id.
This still suffers from the problem that the caller needs to know details that should not concern him.
$a->link('b', $b')
should always be the same as $b->link('a', $a)
.
A possible but complex solution would be to use event handlers to figure this out after the models are saved, this could be troublesome with foreign keys in case models are not saved in the right order.
I'd like to get a discussion going on possible alternative solutions for 2.1.
Somewhere in controller etc.
// For example, start === 10, end === 18
$model->start = 12;
$model->save();
EVENT_AFTER_UPDATE
handler
if ($this->isAttributeChanged("start") || $this->isAttributeChanged("end")) {
echo "Interval changed from {$this->getOldAttribute("start")} - {$this->getOldAttribute("end")} to {$this->start} - {$this->end}";
}
Interval changed from 10 - 18 to 12 - 18
Nothing printed, old attributes were reset and isAttributeChanged
returns false.
To make it happen I'll need to use array_key_exists('parent_id', $changedAttributes) many times, some constructs like isset($changedAttributes['start']) ? $changedAttributes['start'] : $this->start
etc. All that make code much less readable and introduces lots of duplicated code.
Problem is here
active-record/src/BaseActiveRecord.php
Line 822 in 8200ac5
namespace app\commands;
use app\models\Model1;
use Yii;
use yii\base\Exception;
use yii\console\Controller;
class TestController extends Controller
{
public function actionIndex()
{
$model = new Model1();
$model->... = '...'; // fill with correct values
try {
Yii::$app->db->transaction(function () use (&$model) {
$model->save();
throw new Exception('Another error occurred.');
});
} catch (\Throwable $e) {
$this->stdout($e->getMessage() . PHP_EOL);
$this->stdout($model->id . PHP_EOL);
}
}
}
Empty $model->id
There is a value in $model->id
Q | A |
---|---|
Yii version | 2.0.14 |
PHP version | PHP 7.0.27-1+ubuntu16.04.1+deb.sury.org+1 (cli) (built: Jan 5 2018 14:12:46) ( NTS ) |
Operating system | Ubuntu 16.04.3 LTS |
$query = ArClass::find();
$query->addSelect('extraField');
$item = $query->one();
$item
has all default fields + 1 extraField
, defined in addSelect()
$item
has only extraField
and does not have default fields.
Q | A |
---|---|
Yii version | 2.0.13.1 |
PHP version | 7.0.23 |
Operating system | Ubuntu 16.04.3 x86_64 |
select
property means "all by default" yii\db\Query:58 /**
* @var array the columns being selected. For example, `['id', 'name']`.
* This is used to construct the SELECT clause in a SQL statement. If not set, it means selecting all columns.
* @see select()
*/
public $select;
if ($this->select === null) {
$this->select = $columns;
} else {
$this->select = array_merge($this->select, $columns);
}
Some use cases:
What I want | How do I do | Is goal Reached? |
---|---|---|
Select all default columns | ArClass::find()->all() // no call select() |
yes |
Select only 2 passed columns | ArClass::find()->select(['col1', 'col2'])->all() |
yes |
Select 2 passed columns +1 more | ArClass::find()->select(['col1', 'col2'])->addSelect('col3')->all() |
yes |
Select all default columns + 1 more | ArClass::find()->addSelect('col3')->all() |
no |
Select all default columns + 1 more | ArClass::find()->select('*')->addSelect('col3')->all() |
sometimes |
Select all default columns + 1 more | ArClass::find()->select(ArClass::tableName() . '.*')->addSelect('col3')->all() |
yes |
In my opinion, query must select all default columns until I define columns set manually. Add meand adding element to some set. $select
defined as all by default, but when you call addSelect()
without calling select()
, that means default not exists, add this.
See also #12249
#14238
What about AR function?
yii\db\BaseActiveRecord
/**
* Get a relation from the another class relation definition
* @param ActiveRecord|array|callable|string $target the model class who have relation
* @param string $relationName relation name in target (case sensitive, without 'get')
* @return ActiveQuery
*/
public function hasRelation($target, $relationName){
$target instanceof ActiveRecord || $target = Yii::createObject($target);
/** @var ActiveQuery $relationQuery */
$relationQuery = $target->getRelation($relationName);
$relationQuery->primaryModel = $this;
return $relationQuery;
}
class Order extends ActiveRecord
{
public function getItems(){
return $this->hasRelation(OrderItem::className(), 'item')->via('orderItems');
}
public function getOrderItems()
{
return $this->hasMany(OrderItem::className(), ['order_id' => 'id']);
}
}
It's avoid double declare link
and type of relation (multiple
)
When declaring a 'via' (not 'viaTable') relation the respsective behaviors of link()
vs unlink()
are a bit inconsistent.
link()
will trigger events and use behaviors on the junction model provided by the 'via' modelClass
while unlink()
will not.
I think it is really awesome that link()
uses insert()
on the junction model, but conversely unlink()
should also use delete()
on the junction model.
Docs for \yii\db\BaseActiveRecord::save()
@return boolean whether the saving succeeded (i.e. no validation errors occurred)
As far as I can tell there are three ways that it can return false
beforeSave()
returns false
\PDOStatement::rowCount()
is falsy, e.g. integer zero. (This branch is only on insert and does not exist if save()
calls update()
).It seems to me that an INSERT command that executes without an exception but that affects zero rows is an exceptional condition that the app will want to handle differently from validation failure or beforeSave()
returning false
. If this happens I would probably want to rollback the transaction. But it can be complicated for the app code to discriminate this condition and throw an exception.
If \yii\db\Schema::insert
would instead throw an exception then application scripts can more easily deal with save()
returning false
. Validation and beforeSave()
are entirely the app's responsibility. Zero rows affected after insert, otoh, is very weird and better processed via an exception.
It's unusual for false
return in the Yii 2 API to be so ambiguous.
Porposal: change \yii\db\Schema::insert
to throw an exception if !$command->execute()
.
I have several tables that have columns with same name. Let's say shelf
, book
, chapter
, have the same field name
.
I have a page that will show list of chapter
that will also show the name of the book
as well as the shelf
.
Naturally this is the query I used for the ActiveDataProvider
Chapter::find()
->joinWith([
'book' => function($query) {
$query->joinWith([
'shelf'
])
}
])
But I only wanted to show the name in the GridView
, so this is what I do to avoid the SELECT *
.
Chapter::find()
->addSelect(['name', 'bookId'])
->joinWith([
'book' => function($query) {
$query->addSelect(['name', 'shelfId'])
->joinWith([
'shelf' => function($query){
$query->addSelect(['name']);
}
]);
}
])
On my Yii2 (c21895d4dd4c18d5bab37e0b7b359668f8aa8b67) this will output error Column 'name' in field list is ambiguous
. So I have to put something like this
Chapter::find()
->addSelect(['Chapters.name', 'bookId'])
->joinWith([
'book' => function($query) {
$query->addSelect(['Books.name', 'shelfId'])
->joinWith([
'shelf' => function($query){
$query->addSelect(['Shelves.name']);
}
]);
}
])
Do I really have to do this for every query like this? Or is there any simpler way I don't know?
I'm thinking that if I can disambiguate the table name right from the addSelect
method, it would be much easier. I extend the ActiveQuery
and do something like this.
private $_tableName;
private function getTableName() {
//since the `from` will be resolved in the `prepare` method.
if (!isset($this->_tableName)) {
$class = $this->modelClass;
$this->_tableName = $class::tableName();
}
return $this->_tableName;
}
public function addSelect($columns) {
if (is_array($columns)) {
$columns = array_map(function($column) {
return (strpos($column, ".") == false) ? $this->getTableName() . ".{$column}" : $column;
}, $columns);
} else {
$columns = (strpos($column, ".") == false) ? $this->getTableName() . ".{$columns}" : $columns;
}
return parent::addSelect($columns);
}
This is a feature request to add the ability to select the transaction isolation level inside the transactions() method. Currently only the default isolation level is possible when specifying transactions in this manner.
Note: While it is theoretically possible to change the isolation level after the transaction has already begun (for example in beforeSave()), not all DBMS allow this.
The following code yields an unexpected behavior:
public function getCity()
{
return $this->hasOne(Cities::className(), ['id' => 'city_id'])
->via('orderPts',function($q){
$q->andWhere(['order_pts.idx' => 1]);
})
}
The problem is due to callable in via
. If I use $model->city
the result of the city
relation is cached together with the additional condition from the callable. Then all requests to $model->orderPts
return the result cached by $model->city
with additional condition.
I have 2 AR models: User
and Role
connected via junction table. I need to ensure that only certain Role
models attached to User
model. IDs of roles are given.
In Laravel I can write:
$ids = [1, 2, 3];
$user->roles()->sync($ids);
Now, in Yii I do the same thing this way:
$ids = [1, 2, 3];
$user->unlinkAll('role', true);
$roles = Role::find()->where(['id' => $ids)->all();
foreach ($roles as $role) {
$user->link('role', $role);
}
Is it possible to sync links between models without unnecessary creation of Role
entities and deleting all previous connections?
refactor joinWith internals:
related to #10253
yii2 REST API uses ActiveDataProvider
to fetch and show data. This class has pageSizeLimit
parameter which by default limit our pageSize
to 50, so no matter how high is per-page (pageSizeParam
) query parameter, maximum 50 rows are returned.
Is there any way to completely disable pageSizeLimit
so that provider will show as many rows as we want by specifying per-page query parameter or defaultPageSize
if not specified? Setting pageSizeLimit
to false forces to always return defaultPageSize
rows no matter what we pass by per-page.
Further investigation of source code showed, that setting pageSizeLimit
to anything but false
or two-dimensional array works, because the code in that case completely ignores pageSizeLimit
link.
Won't it be better if we have framework proposed way of doing it and not hack, e.g. specifying only lower limit, or setting it true
or something to obviously take the query param? I can make pull request if we come up with the way of specifying it...
Sometimes you want to get all records from table (for example if it is a dictionary).
Each model has findAll($condition) method, but you can call it without condition.
I suggest modify Model::findAll($condition = null) to call it without $condition if you want.
Just Model::findAll();
For example:
$records = Dictionaty::findAll(['field' => 'value']);
// find all records the same way
$records = Dictionary::findAll();
For now it looks like:
$records = Dictionaty::findAll(['field' => 'value']);
// for now it is a different way
$records = Dictionary::find()->all();
class Item extends yii\db\ActiveRecord {
public function getList()
{
return [1, 2, 3 / 0]; // division by zero!
}
}
$list = $item->list ?? [];
var_dump($list); // array(0) {} where is exception?
$list = $item->getList() ?? []; // exception - ok
$list = $item->list ?? []; // Exception division by zero
$list = $item->list ?? []; // array(0) {}
Reason in:
https://github.com/yiisoft/yii2/blob/master/framework/db/BaseActiveRecord.php#L336-L342
??
- operator first make isset()
, so it calls __isset()
, which suppresses errors.
The problem is that it is very difficult to detect an error, because they are suppressed.
Q | A |
---|---|
Yii version | all |
PHP version | 7.0 |
Operating system | all |
Basically it will take the relation name, the ids to set and will insert/delete from the database accordingly.
Here is the code I am using on my ActiveRecord
/**
* @param array $ids
* @param string $name The relation name
* @param boolean $delete Whether to delete the model that contains the foreign key.
*/
public function setRelation($name, $ids, $delete = false)
{
$relationQuery = $this->getRelation($name);
$modelClass = $relationQuery->modelClass;
$primaryKey = $modelClass::primaryKey();
$relationQuery->select($primaryKey);
foreach ($modelClass::find()
->andWhere(['in', $primaryKey, $ids])
->andWhere(['not in', $primaryKey, $relationQuery])
->each() as $model
) {
$this->link($name, $model);
}
foreach ($relationQuery->andWhere(['not in', $primaryKey, $ids])
->each() as $model
) {
$this->unlink($name, $model, $delete);
}
}
Example 1:
/* @var $model common\models\Restaurant */
$model = $this->findModel($id);
/*
* It will link the models with id 1,5,6 (if not there already)
* and delete those that already exist and the id can not pass on the list.
*/
$model->setRelation('usersAssist', [1,5,6]);
Example 2:
/* @var $model common\models\Form */
$model = $this->findModel($id);
$model->setRelation('usersBcc', [2,3]);
$model->setRelation('usersCc', [5]);
can we implement something like this on the core?
The magic __get($name)
override in BaseActiveRecord
is confusing, and its combination of private _attributes
and _related
array access and strange use of $this->hasAttributes($name)
renders it inextensible.
public function __get($name)
{
if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
return $this->_attributes[$name];
} elseif ($this->hasAttribute($name)) {
return null;
} else {
if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) {
return $this->_related[$name];
}
$value = parent::__get($name);
if ($value instanceof ActiveQueryInterface) {
return $this->_related[$name] = $value->findFor($name, $this);
} else {
return $value;
}
}
}
This line..
... elseif ($this->hasAttributes($name)) {return null;}
.. might return the correct value for the framework, but it makes no sense in a getter. Also if the hasAttributes($name)
function is extended to include extra attributes, returning null here is not expected.
I would suggest the following replacement, which as far as I can tell produces the same logic, but allows for much greater extensibility:
public function __get($name)
{
if ($this->hasAttribute($name)) {
return $this->getAttribute($name);
} else {
if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) {
return $this->_related[$name];
}
$value = parent::__get($name);
if ($value instanceof ActiveQueryInterface) {
return $this->_related[$name] = $value->findFor($name, $this);
} else {
return $value;
}
}
}
ActiveQuery::one() doesn't seem to limit records in SQL.
Queries created with ActiveQuery::one() should have a LIMIT 1
clause in the prepared SQL.
The function selects all rows from a table resulting in excess memory consumption.
Q | A |
---|---|
Yii version | 2.0.? |
PHP version | |
Operating system |
@rustamwin commented on Oct 9, 2018, 10:23 AM UTC:
This issue was moved by samdark from yiisoft/yii-core#39.
@SamMousa commented on Oct 25, 2018, 10:12 AM UTC:
Currently yii\db\ActiveQuery
operates in 2 contexts (this is documented in the PHPDoc):
This is a violation of the SRP and I don't think there's a need for it.
For example, ActiveQuery
class has a $primaryModel
property, and a lot of functions change their behavior based on whether or not it is set.
I think we should split this class up into 2 classes, I realize there's some stuff to figure out, that's what this issue is for :)
This issue was moved by samdark from yiisoft/yii-core#53.
Not sure if it should be like this but if find()
is overridden with a default condition, this will affect refresh() so that it will return false even if the row exists, but just does not match the condition anymore. I would expect refresh() to refresh the record regardless of any default condition.
Hello,
how to use subjects with joining tables? Ex:
I have Query which representing sql:
SELECT `t`.* FROM `ss_profiles_finances` `t` LEFT JOIN `ss_profiles` `profile` ON `t`.`profile_id` = `profile`.`id` WHERE `profile`.`login` = 'user1455'
I need to update some fields in ss_profiles_finances
with params of first query, in sql:
UPDATE `ss_profiles_finances` `t` LEFT JOIN `ss_profiles` `profile` ON `t`.`profile_id` = `profile`.`id` SET `t`.`settings`=IF( `t`.`settings` IS NULL, 31, `t`.`settings` | 1 ) WHERE `profile`.`login` = 'user1455'
But in the current implementation I can do only
// $query was cached and serialized in previous operation (another request)
$rows = $this->getModel()->updateAll(
[
'settings' => $helperAddFlag( $opts[ 2 ], $this->getModel() )
],
$query->where,
$query->params
);
which generating following sql:
UPDATE `ss_profiles_finances` SET `settings`=IF( `settings` IS NULL, 31, `settings` | 1 ) WHERE `profile`.`login` = 'user1455'
Of course it is not work without joning ss_profiles
table.
Hi! I have idea - add method to \yii\db\ActiveRecord. I think it conveniently.
/**
* @return bool
*/
public function hasChanges(): bool
{
return sizeof($this->getDirtyAttributes()) > 0;
}
And, maybe, rename dirtyAttributes() to changes() or changedAttributes()?
usually i link 1:n models with its "link" method.
if i use a pivot table, i have to use on more "save" command on the related model.
i am not sure if this is a bug or a feature, but i would find it more conclusive to avoid the additonal save.
here the example scenario:
setup db, creates tables "test", "testline" and "test_testline" (attention: drop table) :
DROP TABLE IF EXISTS `test`;
CREATE TABLE IF NOT EXISTS `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`date1` date NOT NULL,
`msg` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `test` (`id`, `date1`, `msg`) VALUES
(1, '0000-00-00', 'parent item');
DROP TABLE IF EXISTS `testline`;
CREATE TABLE IF NOT EXISTS `testline` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`test_id` int(11) NOT NULL,
`info` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `testline` (`id`, `test_id`, `info`) VALUES
(1, 1, 'item #1'),
(2, 1, 'item #2');
DROP TABLE IF EXISTS `test_testline`;
CREATE TABLE IF NOT EXISTS `test_testline` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`test_id` int(11) NOT NULL,
`testline_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `fk_test_idx` (`test_id`),
KEY `fk_testline_idx` (`testline_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=3 ;
INSERT INTO `test_testline` (`id`, `test_id`, `testline_id`) VALUES
(1, 1, 1),
(2, 1, 2);
ALTER TABLE `test_testline`
ADD CONSTRAINT `fk_test` FOREIGN KEY (`test_id`) REFERENCES `test` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
ADD CONSTRAINT `fk_testline` FOREIGN KEY (`testline_id`) REFERENCES `testline` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION;
Test-Model with two different relationships, doing almost the same thing:
class Test extends \yii\db\ActiveRecord
{
public static function tableName()
{
return 'test';
}
public function rules()
{
return [
[['msg'], 'string', 'max' => 255],
];
}
public function getTestlines()
{
return $this->hasMany(Testline::className(), ['id' => 'testline_id'])
->viaTable('test_testline', ['test_id' => 'id']);
}
public function getTestlinesWorking()
{
return $this->hasMany(Testline::className(), ['test_id' => 'id']);
}
}
Testline Model:
class Testline extends \yii\db\ActiveRecord
{
public static function tableName()
{
return 'testline';
}
public function rules()
{
return [
[['info'], 'required'],
];
}
public function getTest()
{
//return $this->hasOne(Test::className(), ['id' => 'test_id']);
return $this->hasOne(Test::className(), ['id' => 'test_id'])->viaTable('test_testline', ['testline_id' => 'id']);
}
}
in a test view:
$model = Test::findOne(1);
$newline = new Testline;
$newline->info = 'created by code (direct link)';
// working direct link, should return n+1 rows
$transaction = $model->getDb()->beginTransaction();
$model->link('testlinesWorking', $newline);
foreach ($model->testlinesWorking as $key => $item) {
var_dump($item->attributes);
}
$transaction->rollback();
// --- viaTable example
$newline = new Testline;
$newline->info = 'created by code (linked viaTable)';
// not working viaTable if you don't use save before
$transaction = $model->getDb()->beginTransaction();
// UNCOMMENT THIS LINE TO MAKE IT WORK:
// $newline->save();
$model->link('testlines', $newline);
foreach ($model->testlines as $key => $item) {
var_dump($item->attributes);
}
$transaction->rollback();
Feature Request. Hello. I'm using AR and created a relation for current user's relational record in another table (user's vote result of poll). Relation looks this way:
/**
* @return \yii\db\ActiveQuery
*/
public function getUserPollStatus($id)
{
return $this->hasOne(UserPollStatusItem::className(),
['poll_id' => 'id'])
->where(['user_id' => $id])
->limit(1);
}
I was expecting that could be able to enable eager loading for this query and that this will look like:
if ($user_id !== null)
$query->with(['userPollStatus' => [$user_id]]);
But it looks that this doesn't work, it supports only callbacks.
Also, it does not check really passed value.
What about adding ability to pass an array instead of callback to modify relation query?
Although the result of using where()
after andWhere()
or orWhere()
is known and documented, still it's one of the vulnerable areas that could easily fall through the cracks especially by new team members. It's easy to use where()
in a child class to result in removing user scoping in the parent class and expose sensitive data.
To enhance this area, I'm suggesting the introduction of new method called alwaysWhere()
(or fixedWhere()
or scopeWhere()
or mustWhere()
...etc) with the same signature of where()
but its condition is not affected by where()
, andWhere()
, or orWhere()
. Its condition would always be added to the WHERE
condition in the resulting SQL statement. Taking this further, a parameter like accumulate
could be used ( where($condition, $accumulate = true)
) so by default, any additional use of alwaysWhere()
in the application will lead to adding the new condition to the old one unless accumulate
is explicitly set to false
by the developer to overwrite the scoping condition.
$video = Video::find()->andWhere(['id' => $videoId])
->with(['videoType', 'videoThumbnails'])
->one();
// DATA IS GOOD
$videoArray = $video->toArray([], ['videoType', 'videoThumbnails']);
// do some video manipulations
// now refresh video object
$video->refresh();
// NO RELATIONS!
$videoArray = $video->toArray([], ['videoType', 'videoThumbnails']);
//WORKAROUND
$video->videoType;
$video->vdeoThumbnails;
// DATA IS GOOD NOW
$videoArray = $video->toArray([], ['videoType', 'videoThumbnails']);
repopulate existing/fetched relations
nothing
| ---------------- | ---
| Yii version | 2.0.13
I think, will be better add method findByPk() and remove search without keys for findOne() and findAll().
It would be nice to know, what attributes are going to be saved in beforeSave method. If you have some logic, which fires before saving particular attribute, that logic will fire even if you called save specifiying some other attributes. You may have this attribute locally changed, so checking isAttributeChanged
is not an option. This will complement afterSave changedAttributes logic and as I saw, it won't be very hard to implement this.
Create AR with some attributes including name
and status
Add some code in AR beforeSave
method, which does something with status
attribute
Change status
and name
attributes locally
Call save(true, ['name'])
. (Do not include your attribute to save)
You know, that status will not be saved and act accordingly
You don't know, what attributes will be saved
Q | A |
---|---|
Yii version | 2.0.13 |
PHP version | 7 |
Operating system | any |
I don't know if this will break the Active Record pattern so read carefully. Written in php7 for simplier will be changed to php5 if approved.
interface RelationalRecord
{
/**
* Will store the `$query` into an array indexed by the `$name` parameter
* This will allow creating relations on the fly and later call them with `getRelation()`
*
* usage example:
*
* ```php
* $model->setRelation('bigOrders', $model->getOrders()->andWhere(['>=', 'amount', 1000));
* $model->bigOrders; // calls the query and stores the result on the $model object.
* ```
*/
public function setRelation(string $name, ActiveQuery $query = null);
/**
* finds the relation named `$name`, executes it and returns it, if no query is already
* prepared to turn into this relation then will check if a getter is defined for the
* `$name` property and use that instead.
*
* @return static|static[]
*/
public function getRelation(string $name);
/**
* unsets the ActiveRecords associated to the relation so it can be created again.
*/
public function resetRelation(string $name);
/**
* unsets all relations
*/
public function resetRelations();
/**
* If the relation is still an ActiveQuery it can be modified using this method
*
* @param callable $callback with the signature `function (ActiveQuery $query){}`
*/
public function modifyRelation(string $name, callable $callback);
}
With this methods we can modify relations on the fly and simplify how relations are handled right now.
I also think this can be implemented for the 2.0 branch but i think @cebe should confirm it.
Create tryFind(), tryFindOne(), tryFindByPk() wrappers. Wich will generate not found exception, if element not found.
It`s more simple to log errors.
Example from documentation: http://www.yiiframework.com/doc-2.0/guide-db-active-record.html
// "id" and "email" are the names of columns in the "customer" table
$customer = Customer::findOne(123);
$id = $customer->id;
$email = $customer->email;
in bad case will failed on line $customer->id; instead of correct error in logs.
I think it will be very helpful to implement in ActiveRecord method that return state that shows is property with some name was changed. Somethink like:
public function isPropertyChanged($propertyName) {
return ArrayHelper::keyExists($propertyName, $this->dirtyAttributes);
}
Link models
I expect the the function to link models without saving the foreign model.
When calling the link() method that later calls the private bindModels() function, it saves the foreign model without validating it. In my case it is not a required effect, I would like to be able to save it manually.
Example: I have problems with validating model by event (EVENT_BEFORE_VALIDATE) - because this event doesn't have info about attributes we need to validate.
it is weird why findAll() method should behave different from all other xxxAll()
Table::findAll(
[
'id' => 123,
'check' => true,
['OR', ['userId' => 123], ['userId' => 0]],
]
);
SELECT * FROM `table` WHERE `id` = 123 AND check = 'true' AND (userId = 0 OR userId = 123)
SELECT * FROM `table` WHERE `id` IN (123, 'true', NULL)
deleteAll command work as expected and its really weird why thing behave differently and if it want to change the query why it doesn't just throw an exception. like where() which throw exception when we call it with same array
Table::find()->where(
[
'id' => 123,
'check' => true,
['OR', ['userId' => 123], ['userId' => 0]
],
])->all();
Q | A |
---|---|
Yii version | 2.0.13 |
PHP version | 7.1 |
Operating system | Debian 9 |
Currently the activerecord implementation gives a higher priority to attributes (and relations) than it gives to setters and getters.
Often it is desired to overwrite an attribute (from the PHP point of view) using a getter and setter; since we can directly set an attribute using setAttribute.
Currently there are some issues with how writing to attributes is handled:
Some improvements could be made:
Currently ActiveRecord breaks a contract set by Object; from the docs:
Object is the base class that implements the property feature.
This is a feature request, not a bug. Currently, AR does only AND across Active Query methods. What if we would like to OR, NOT among those methods?
Let me explain with an example.
Lesson::find()->scheduled()->all();// fetch all scheduled lessons
Lesson::find()->completed()->all();// fetch all accepted lessons
Lesson::find()->canceled()->all();// fetch all canceled lessons
I'd like to do something like this to fetch scheduled or completed lessons
Lesson::find()->scheduled()->or()->completed()->all();
or
Lesson::find()->or(function($query) {
$query->scheduled();
$query->completed();
});
Rails: https://stackoverflow.com/questions/3639656/activerecord-or-query
http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-or
I cannot even count how many times I've encountered this issue.
I want to call SomeRecord::findAll($pks)
where $pks
is an array of primary keys.
However, I just want to pass another condition into that query or set a custom index()
or different order()
... But I can't because findAll()
goes straight to all()
.
Ok so, let me just call SomeRecord::findByCondition($pks)->myCondition()->all()
then... Well darn, I can't do that either since it's protected
.
Could we make findByCondition()
public? Either that or add a second callable $query
parameter to findAll()
and findOne()
(similar to how it's done in Query::with()
)?
I'm struggling every time with the DateTime
handling in queries and ActiveRecord
s. The problem is, that the Schema
does not handle any DateTime
conversion, it could be allot easier if it would.
In a form the DateTime
value could have any format and the Model::load()
method does just insert it with this format. It is possible to do a format check with a rule, but that does not solve this problem.
I've tested some possible solutions:
__set()
and __get()
methods to do a conversion to a custom DateTime
object which handles __toString()
to convert it in the DB related format.Behavior
.The problem with this solutions is, that if the DB changes it could be possible that it accepts only another format. There are many other problems, because both solutions change the original value to something else. So my thought was to use the yii\db\Schema
for the conversion. So it would be possible to convert it to the expected DB format and do not touch the model.
Another thought was to convert any datetime-field to a DateTime
object. This would overcome a problem which would occur, if a value is handled on the client side like this MM-yyyy
, which does not contain a day and could not be converted without this specific description. It would be handy if it would convert the date with the rule definition, so both sides are handled.
If I understand correctly, here it is possible to leave wishes to the third version of the framework?
I very much need possibility to register the getter or the setter redefining default for any property of a model storing in a database
Now you have to do it through filters or afterFind(). This is not convient
That's how it looks like, for example, laravel: https://laravel.com/docs/5.6/eloquent-mutators#accessors-and-mutators
Если я правильно понимаю, здесь можно оставить пожелания к третьей версии фреймворка?
Очень не хватает возможности прописать свой геттер или сеттер переопределяющий дефолтный для какого-то свойства модели хранящего в бд
Приходится изворачиваться через фильтры или afterFind
Вот так это выглядит например, у laravel: https://laravel.com/docs/5.6/eloquent-mutators#accessors-and-mutators
$query = Product::find()->where(['id' => 1]);
$query2 = Product::find()->where(['id' => 2]);
$query-> union($query2);
$provider = new ActiveDataProvider([
'query' => $query,
'pagination' => [
'pageSize' => 12,
],
]);
$provider->getModels();
(SELECT * FROM product
WHERE id
=1) UNION ( SELECT * FROM product
WHERE id
=2 ) LIMIT 12
(SELECT * FROM product
WHERE id
=1 LIMIT 12) UNION ( SELECT * FROM product
WHERE id
=2 )
Q | A |
---|---|
Yii version | 2.0.? |
PHP version | |
Operating system |
my understanding of a relational database is that there can be many kinds of relations, yet yii only supports single table relations. i find this extremely limiting.
for example, i have tags that can be assigned to an item via an ItemTag table and also via a CustomTag table. in my item, i want to define a single getTags() relation that gets all the tags assigned via both tables and sorts them on a condition. as far as i can tell yii is unable to perform such a task inherently. i would have to get both tag lists separately and sort them manually in php.
$popularityQuery = (new \yii\db\Query())
->select( ['COUNT(*) AS popularity', 'tag_id'] )
->from( 'item_tag' )
->groupBy( 'tag_id' );
$this->hasMany( Tag::className(), ['id' => 'tag_id'] )
->viaTable( ItemTag::tableName(), ['item_id' => 'id'] )
->leftJoin( ['totals' => $popularityQuery], 'totals.tag_id = tag.id' )
->orderBy( 'popularity DESC' );
$this->hasMany( Tag::className(), ['id' => 'tag_id'] )
->viaTable( CustomTag::tableName(), ['item_id' => 'id'] )
->leftJoin( ['totals' => $popularityQuery], 'totals.tag_id = tag.id' )
->orderBy( 'popularity DESC' );
The latest updates from several DB such as MySQL, Oracle, PostgreSQL are all supporting JSON data type with basic operations: create, update, index, validate.
Should we implement a support at PHP layer of Yii Framework to support those JSON operations?
Is it possible now (and how ?), is it planned, or how can I do it myself ?
Let's assume we have models Customer
, Contract
,Department
, JobPosition
, Worker
, NotifyChannel
all having defined relations from left to right.
In UltimateDisasterController
, i want to have contacts of all workers involved with particular Customer
, without traversing all chain in nested loops, or poisoning models with combinatory explosion of deep relations.
$arObNotifyChannel = $obCustomer->deepVia([/* ... */]);
Есть 3 таблицы:
Основные:
TableA: id_a, data
TableB: id_b,data
Таблица связей (viaTable):
TableC: id_a, id_b, type
метод link() позволяет создать ссылку по типу:
$modelA->link('relAB', $modelB, ['type' => 1]);
$modelA->link('relAB', $modelB, ['type' => 2]);
что весьма удобно
Метод unlink() не может удалить ссылку с определённым type, он удаляет только всё.
Хотелось бы видеть идентичное поведение у данных методов, а именно возможность задать в unlink() дополнительные параметры.
Q | A |
---|---|
Yii version | 2.0.15.1 |
PHP version | 7.0 |
Operating system | Debian 9 |
Trying to get count of records for relation with viaTable
public function getPictures()
{
return $this->hasMany(Picture::className(), ['id_picture' => 'id_picture'])
->viaTable('dbl_album_picture', ['id_album' => 'id_album']);
}
like that
$album->getPictures()->count();
SELECT count(*) FROM dbl_album_picture
WHERE id_album
=1
SELECT * FROM dbl_album_picture
WHERE id_album
=1
SELECT COUNT(*) FROM db_picture
WHERE id_picture
IN ('13', '14', '16', '17', '18', '19', '20')
Q | A |
---|---|
Yii version | 2.0.12 |
PHP version | PHP 7.0.8 |
Currrently there is no possibility to dynamically manage relations that should be included to respond in case when relation level is bigger than one.
For example we have model Business and you can define Locations relation like this:
public function extraFields()
{
$fields = parent::fields();
$fields[]='locations';
return $fields;
}
And then you can include it to respond using ?expand="locations"
But if we want to include Locations together with related location Schedules, then that doesn't work.
I suggest to implement multi-level relations (using dot notation):
public function extraFields()
{
$fields = parent::fields();
$fields[]='locations';
$fields[]='locations.schedule';
return $fields;
}
And then it will be possible to use it this way: ?expand="locations, locations.schedule"
I think it would be nice, because it's very often needed to manage relation records that should be included to respond.
Is it possible?
Currently ActiveDataProvider takes several arguments: query, sort, pagination and then inside function prepareModels() it builds final query and executes it:
https://github.com/yiisoft/yii2/blob/master/framework/data/ActiveDataProvider.php#L99
After that I want to get that final query. If building final query (adding things for pagination and sorting) could be moved to another (public) function then it would easier to use it.
Right now the workaround could be to extend ActiveDataProvider and overload prepareModels(). Quite a cumbersome solution for something that could be solved with simple refactoring.
Q | A |
---|---|
Yii version | 2.0.14-dev |
PHP version | 7.1.12 |
Operating system | Debian 7.3.0-1 |
Currently, \yii\db\ActiveQuery
allows you to join the same relations twice (or more) with joinWith
. Because of that, the DBMS will complain as you are doing multiple joins on the same table.
I am wondering why this isnt fixed with a simple patch to
in which $joinWith
gets the relations mapped by their relation $key. This prevents double insertions and those problematic and unnecessary exceptions when you are ensuring that a joinWith is executed.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.