yiisoft / active-record

Active Record database abstraction layer
https://www.yiiframework.com/
BSD 3-Clause "New" or "Revised" License
69 stars 29 forks source link

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

Open e-frank opened 10 years ago

e-frank commented 10 years ago

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();
cebe commented 10 years ago

How do you expect the entry in the relation table to be created when the model has no primaryKey value assigned?

e-frank commented 10 years ago

sure you are right. i wished i found some automagic here and expected a 'linked save' to save the complete model-tree. [or the same behaviour with and without ->viaTable. with viaTable the child model is saved, without isn't.]

when you use ->link a second time, you will get an additional row in the relation table, and no error, if you don't have an unique index. i didn't find anything like a isLinked method, so what i think i have to do is test the parent relationship for an existing child by the child's primary key, like:

$hasChild = $parent->getRelation($relation)->andWhere('id=1')->exists();

and then decide if you need ->link. i am working on a hydrator and hope to publish it asap. still: fantastic work!

dynasource commented 7 years ago

closed in favor of https://github.com/yiisoft/yii2/issues/4834

cebe commented 7 years ago

@dynasource this issue is #4834 :)

dynasource commented 7 years ago

:), if this was an algorithm, all the issues would have been closed at once

samdark commented 5 years ago

https://github.com/yiisoft/yii2/pull/15882