Code Monkey home page Code Monkey logo

active-record's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

active-record's Issues

Static relations

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:

  • One definition for lazy and greedy loading
  • Support (cached) retrieval via magic getter
  • Support getting a query object (currently via the getter that defines it)

Pros of current approach:

  • Uses "native" magic getters (with a small extension)
  • Has code completion for the getters since they are normal functions

Cons:

  • No way to get the relationships without instantiating a dummy model
  • No way to enumerate relations

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:

  • build a lazy query
  • build a greedy query
  • enumerate via reflection

From a consumer point of view nothing changes:

  • getCustomers via __invoke()
  • ->customers via __get()

Pros:

  • Relations are now static
  • No need for dummy instances
  • OO definition of relationship unrelated to the query object

Cons:

  • no auto complete for the getter, so could require additional annotations.
  • breaks BC

[2.1][Discussion] ActiveRecord::link behavior

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.

Reset _oldAttributes after afterSave.

What steps will reproduce the problem?

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}";
}

What is the expected result?

Interval changed from 10 - 18 to 12 - 18

What do you get instead?

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.

Additional info

Problem is here

$this->_oldAttributes[$name] = $value;

There were lots of such issues before, like this and this. And while it's possible to use current logic or tweak it yourself, I think a proposed approach is more straightforward.

Db trans rollback but leave model primary key exists

What steps will reproduce the problem?

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);
        }
    }
}

What is the expected result?

Empty $model->id

What do you get instead?

There is a value in $model->id

Additional info

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

Calling yii\db\Query addSelect($fields) overwrites default select set

What steps will reproduce the problem?

$query = ArClass::find();
$query->addSelect('extraField');
$item = $query->one();

What is the expected result?

$item has all default fields + 1 extraField, defined in addSelect()

What do you get instead?

$item has only extraField and does not have default fields.

Additional info

Q A
Yii version 2.0.13.1
PHP version 7.0.23
Operating system Ubuntu 16.04.3 x86_64
  1. Due to docs, 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; 
  1. Add statement ignores defauls if I did not defined defaults again. yii\db\Query:592
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

Idea: avoid double declare link and type of relation if used via()

#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)

ActiveRecord unlink() not applying events/behaviors on junction model - inconsistent with link()

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.

\yii\db\BaseActiveRecord::save() returns false if \PDOStatement::rowCount() is falsy after insert

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

  1. validation fails
  2. beforeSave() returns false
  3. after an INSERT command is executed, \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().

Disambiguing column's table on ActiveQuery select/where/order etc.

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);
    }

ActiveRecord transactions() ability to set isolation level

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.

Faulty caching in AR relations with via and callable

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.

Laravel's sync() analog in Yii

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() storing and processing

refactor joinWith internals:

  • store one join in $joinWith property instead of the whole array. i.e. normalize beforehand!
  • makes a lot of the aliasing much cleaner and simpler.

related to #10253

Any proposed way of setting or disabling pageSizeLimit

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...

Suggestion: ActiveRecord::findAll() without $condition

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();

ActiveRecord Magic and ?? operator

What steps will reproduce the problem?

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

What is the expected result?

$list = $item->list ?? []; // Exception division by zero

What do you get instead?

$list = $item->list ?? []; // array(0) {}

Additional info

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

ActiveRecord::setRelation() method

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]);

captura de pantalla de 2015-07-21 22 17 18

can we implement something like this on the core?

Magic getter in BaseActiveRecord has readability and extensibility issues

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() limiting database records

What steps will reproduce the problem?

ActiveQuery::one() doesn't seem to limit records in SQL.

What is the expected result?

Queries created with ActiveQuery::one() should have a LIMIT 1 clause in the prepared SQL.

What do you get instead?

The function selects all rows from a table resulting in excess memory consumption.

Additional info

Q A
Yii version 2.0.?
PHP version
Operating system

Funding

  • You can sponsor this specific effort via a Polar.sh pledge below
  • We receive the pledge once the issue is completed & verified
Fund with Polar

Split ActiveQuery

@SamMousa commented on Oct 25, 2018, 10:12 AM UTC:

Currently yii\db\ActiveQuery operates in 2 contexts (this is documented in the PHPDoc):

  • Normal
  • Relational

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.

AR::refresh() takes default condition into account

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.

ActiveRecord::update() and ActiveRecord::updateAll() with joining tables.

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.

Add `hasChanges` to AR

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()?

ActiveRecord "link" requires additional "save" when linked "viaTable" otherwise "Unable to link models: both models must NOT be newly created" error

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();

FR ActiveRecod relation parameters in with() call as array

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?

Avoiding overwriting andWhere()/orWhere() with where() in ActiveQuery when scoping data

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.

AR refresh() with relations

What steps will reproduce the problem?

$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']);

What is the expected result?

repopulate existing/fetched relations

What do you get instead?

nothing

Additional info

| ---------------- | ---
| Yii version | 2.0.13

Add findByPk()

I think, will be better add method findByPk() and remove search without keys for findOne() and findAll().

AR beforeSave attributes

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.

What steps will reproduce the problem?

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)

What is the expected result?

You know, that status will not be saved and act accordingly

What do you get instead?

You don't know, what attributes will be saved

Additional info

Q A
Yii version 2.0.13
PHP version 7
Operating system any

[Proposal] for branch 2.1 Refactoring idea for handling relations in Active Record

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.

TryFind functions. Generate exception if elements not found.

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.

add method that check is property was changed in ActiveRecord model

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);
}

BaseActiveRecord::bindModels() doing unnecessary save

What steps will reproduce the problem?

Link models

What is the expected result?

I expect the the function to link models without saving the foreign model.

Additional info

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.

weird behavior of findAll

it is weird why findAll() method should behave different from all other xxxAll()

Table::findAll(
  [
    'id' => 123,
    'check'  => true,
    ['OR', ['userId' => 123], ['userId' => 0]],
  ]
);

What is the expected result?

SELECT * FROM `table` WHERE `id` = 123 AND check = 'true' AND (userId = 0 OR userId = 123)

What do you get instead?

SELECT * FROM `table` WHERE `id` IN (123, 'true', NULL)

Additional info

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

ActiveRecord::__get priority

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:

  1. There are several places where the _attributes array is directly accessed, this means I cannot control it by overriding setAttribute (since that one gets bypassed in populate and __get / __set).
  2. I cannot override a database field using getters and setters in my model, for example getCreated() to return a datetime object instead of a string. (Currently it can be done by using other names for the database field and the getter, but that is not desirable).

Some improvements could be made:

  1. Only write to $_attributes via setAttribute.
  2. Prioritize getXX and setXX over relations in magic getters and setters.

Currently ActiveRecord breaks a contract set by Object; from the docs:

    Object is the base class that implements the property feature.

Active Record 'OR' query

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

Make findByCondition() public or allow findOne()/findAll() query condition modification

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())?

DB DateTime handling definition in Schema

I'm struggling every time with the DateTime handling in queries and ActiveRecords. 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:

  • Use a DateTime-Trait and override the __set() and __get() methods to do a conversion to a custom DateTime object which handles __toString() to convert it in the DB related format.
  • Handle it with the before save events, for the DB conversion. This could be done in a 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.

Getters and setters for Activerecord

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

Activedataprovider set limit inside of query with union but should outside

What steps will reproduce the problem?

$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();

What is the expected result?

(SELECT * FROM product WHERE id=1) UNION ( SELECT * FROM product WHERE id=2 ) LIMIT 12

What do you get instead?

(SELECT * FROM product WHERE id=1 LIMIT 12) UNION ( SELECT * FROM product WHERE id=2 )

Additional info

Q A
Yii version 2.0.?
PHP version
Operating system

relations cannot be linked via multiple tables

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' );

Support JSON data type in latest MySQL, Oracle, PostgreSQL

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?

Relations: deep path, ad-hoc via() ?

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([/* ... */]);

ActiveRecord link() / unlink()

Есть 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

count() for relation with viaTable buid two queries with IN (....)

image

What steps will reproduce the problem?

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();

What is the expected result?

SELECT count(*) FROM dbl_album_picture WHERE id_album=1

What do you get instead?

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')

Additional info

Q A
Yii version 2.0.12
PHP version PHP 7.0.8

Suggestion to add multi-level relations to ActiveRecord::extraFields()

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?

Add method getFinalQuery() to ActiveDataProvider

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.

Additional info

Q A
Yii version 2.0.14-dev
PHP version 7.1.12
Operating system Debian 7.3.0-1

Ignore unnecessary joinWiths

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.