【问题标题】:Setter method for Yii2 ActiveRecord relational attributeYii2 ActiveRecord 关系属性的设置方法
【发布时间】:2015-08-27 12:30:12
【问题描述】:

我对 Yii2 活动记录处理关系属性的方式感到困惑。是否可以将一个活动记录对象链接到另一个对象而不先保存它?

例如,我想将图像作为徽标添加到公司记录中,但尚未决定是否应保存这两个记录。给定一家公司

/**
 * @property integer $logo_id
 */
class Company extends \yii\db\ActiveRecord
{
    public function getLogo()
    {
        return $this->hasOne(Image::className(), ['id' => 'logo_id']);
    }
}

还有一张图片

class Image extends \yii\db\ActiveRecord
{
    …
}

有没有一种简单的方法来设置$company->logo = new Image();,以便下次调用$company->save() 时图像与公司一起保存,但不是在此之前?换句话说:我正在寻找相当于 @logo = @company.build_image() in rails 的 Yii2。

就目前而言,我在 Company 模型中定义了一个徽标设置函数。

public function setLogo(Image $image)
{
    // `link` expects the primary key to be present
    $image->id || $image->save();
    $this->link('logo', $image);
}

这显然违反了最小惊讶原则,因为 setter 不仅会修改当前对象,还会以可能意想不到的方式影响(这里:保存)参数。这似乎是个糟糕的主意。

如何在不违背良好设计原则的情况下做到$company->logo = new Image

【问题讨论】:

  • 你在创建公司的时候上传图片吗?
  • @marche 有时,但通常会在以后发生(可能永远不会)。
  • 您可以在您的 Company 模型中添加一个包含临时徽标的属性并稍后对其进行修改,但如果您不解释您希望完成的工作流程类型,我将无法为您提供更多帮助。
  • @marche 感谢您提供检查完整工作流程的提议,但公司政策不允许我在这里讨论此问题。目前,我主要对解决这个特定问题感兴趣。使用单独的属性来保存临时徽标肯定会起作用,但这似乎很不雅。它会给我们两个属性,其中一个就足够了。
  • 您也可以使用一个单独的模型来保存您需要的所有字段,并在最后决定将公司和徽标保存到数据库中时。这个模型可以类似于新 Yii2 安装默认提供的 LoginForm 模型。

标签: activerecord yii2


【解决方案1】:

这些是需要以任何方式发送到数据库的 2 个连续请求。

不违背好的设计原则?

link()unlink() 旨在通过在 db 请求中设置它们的相对外键(在或没有 联结表 )。 unlink() 甚至可以在其 $delete 属性设置为 true 时删除相关记录。因此,必须先使用有效的 id 创建相关数据,然后才能在第二个请求中关联它们。

如果模型无法链接,link() 方法将抛出 yii\base\InvalidCallException。这很好,但在保存受影响的 Active Record 实例时它不会执行任何数据验证。另一方面,save() 方法默认会执行验证,但如果出现问题不会抛出 http 异常。

因此,如果我必须使用 link() 方法,我会在确保相关模型已保存(或通过尝试检索它而存在)后,在 CONTROLLER 操作中链接模型,而不是模型类那么我肯定会在某些事情无法按预期工作时引发异常:

if ($image->save() === false && !$image->hasErrors()) {
    throw new ServerErrorHttpException('Failed for some reason.');
}
$company->link('logo', $image);
  • 您也可以使用 Transaction 如果必须发送多个请求并且应该正确保存或回滚,如果他们不喜欢 in this example

否则我可能会保存相关模型并将其新的 id 属性分配给当前模型。我认为没有必要在模型类中使用 link() 方法并向 db 再发送一个请求,如果我知道我的模型将在以后以任何方式通过调用它的 @ 来保存的话987654333@方法:

public function setLogo(Image $image)
{
    if (!$image->id && $image->save() === false){
        throw new yii\base\UserException('Failed for some reason');
    }
    $this->logo_id = $image->getPrimaryKey();
}

或者在您的控制器中,您可以简单地执行以下操作:

$image = new Image();
if ($image->save()) // or maybe throw exceptions
$company->logo_id = $image->getPrimaryKey();

无需为它编写任何额外的函数。调用$company->save()时会关联


是否可以将活动记录对象链接到另一个对象 不先保存?

另一种方法是将图像实例保存在某个地方,例如公共或私有财产,直到您根据未来的逻辑决定是否要保存它。在这种情况下,将在 insertupdate 之前触发的beforeSave 方法可能是这样做的好地方:

private $_image;

public function getImage()
{
    return $this->_image;
}

public function setImage(Image $image)
{
    $this->_image = $image;
}

public function beforeSave($insert)
{
    if ($this->_image->id){
        $this->logo_id = $this->_image->id;
    }

    // otherwise you may save it as previous examples
    // before assigning its returned id to $this->logo_id
    // it will be good to have also plans for unexpected errors

    return parent::beforeSave($insert);
}

然后在执行$company->image = new Image 时,图像实例将一直存在,直到调用$company->save()$company->update()

【讨论】:

  • 非常感谢您的完整回答!特别是对处理这种情况的惯用方法的解释很棒。来自 Rails,我发现在控制器中做这么多工作是不寻常的,尽管在这里做这件事似乎是合理的。
  • 哦,我很高兴。在回答您的问题的过程中,我确实学到了更多东西!我也对 Rails 如何处理这些情况感到好奇,所以感谢您提出并欢迎来到 Yii!
  • 这引入了很多神奇/模糊的属性。我希望您的保存尽可能多地一对一。
猜你喜欢
  • 2014-06-25
  • 1970-01-01
  • 2015-07-24
  • 1970-01-01
  • 2013-02-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-30
相关资源
最近更新 更多