Laravel与非默认本地密钥有很多关系

时间:2014-05-23 09:53:51

标签: php laravel laravel-4 eloquent

我的架构如下:

客户(hasMany Accounts)

  • id
  • 名称

帐户(hasMany Holdings,belongsTo Clients)

  • id(int)
  • account_id(字符串,唯一键)
  • 名称

控股(所属帐户)

  • ID
  • account_id(字符串)
  • holding_date ... etc

所以,客户有很多账户有很多控股。需要注意的是,帐户的本地密钥为account_id,而不仅仅是预期的id。这是因为要求帐户具有字符串标识符。在馆藏表中,外键也是account_id

我已经定义了我的关系:

// Client.php
public function accounts()
{
    return $this->hasMany('Account');
}

// Account.php
public function client()
{
    return $this->belongsTo('Client');
}
public function holdings()
{
    return $this->hasMany('Holding');
}

// Holding.php
public function account()
{
    return $this->belongsTo('Account', 'account_id', 'account_id');
}

如果我想查询给定客户ID的所有馆藏,我该怎么做?如果我做了像

这样的事情
Client::find($id)->accounts->holdings;

我收到此错误:

  

未定义属性:Illuminate \ Database \ Eloquent \ Relations \ HasMany :: $ holdings

我也尝试使用hasManyThrough关系(已经将关系添加到我的模型中),但似乎只有一种方法来定义外键,而不是帐户的本地键。有什么建议吗?

4 个答案:

答案 0 :(得分:4)

假设您在帐户表上有client_id

这样做:

// Account model
public function holdings()
{  
  return $this->hasMany('Holding', 'account_id', 'account_id');
}

// then

$client = Client::with('accounts.holdings')->find($id);
$client->accounts // collection
          ->first() // or process the collecction in the loop
          ->holdings; // holdlings collection

HasManyThrough仅在Account模型具有(或将具有此目的)$ primaryKey设置为account_id而非默认id

时才有效

由于account_id不是Account模型的主键,因此您无法使用hasManyThrough。所以我建议你这样做:

$accountIds = $client->accounts()->lists('account_id');

//  if it was many-to-many you would need select clause as well:
//  $accountIds = $client->accounts()->select('accounts.account_id')->lists('account_id');

$holdings = Holding::whereIn('account_id', $accountIds)->get();

通过这种方式,您可以按照自己的意愿获得收藏,与急切加载相比,donwside需要再查询一次。

答案 1 :(得分:0)

您需要在帐户模型中更改您的关系

// Account.php
public function client()
{
   return $this->belongsTo('Client','account_id');
}

但是,更适合在client_id

中将列名更改为Accounts

答案 2 :(得分:0)

我认为您可以使用load方法为每个帐户获取相应的结果查询。类似的东西:

Client::find($id)->load('accounts.holdings');

这意味着client_id中存在accounts,而holdings也存在account_id

  

PS:我不确定这在这种情况下会如何起作用。但我希望这可以让你找到实现它的方法。

答案 3 :(得分:0)

你必须稍微改写Eloquent。我刚刚遇到了与BelongsToMany关系非常相似的东西。我试图执行多对多查询,其中相关的本地密钥不是主键。所以我将Eloquent的BelongsToMany扩展了一点。首先为BelongsToMany关系类构建一个覆盖类:

namespace App\Overrides\Relations;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany as BaseBelongsToMany;

class BelongsToMany extends BaseBelongsToMany
{
    protected $localKey;

    /**
     * @var array
     */
    protected $customConstraints = [];

    /**
     * BelongsToMany constructor.
     * @param Builder $query
     * @param Model $parent
     * @param string $table
     * @param string $foreignKey
     * @param string $otherKey
     * @param string $relationName
     * @param string $localKey
     */
    public function __construct(
        Builder $query,
        Model $parent,
        $table,
        $foreignKey,
        $otherKey,
        $relationName = null,
        $localKey = null
    ) {
        //The local-key binding, assumed by Eloquent to be the primary key of the model, will have already been set
        if ($localKey) {    //If it's intended to be overridden, that value in the Query/Builder object needs updating
            $this->localKey = $localKey;
            $this->setLocalKey($query, $parent, $table, $foreignKey, $localKey);
        }
        parent::__construct($query, $parent, $table, $foreignKey, $otherKey, $relationName);
    }

    /**
     * If a custom local-key field is defined, don't automatically assume the pivot table's foreign relationship is
     * joined to the model's primary key.  This method is necessary for lazy-loading.
     *
     * @param Builder $query
     * @param Model $parent
     * @param string $table
     * @param string $foreignKey
     * @param string $localKey
     */
    public function setLocalKey(Builder $query, Model $parent, $table, $foreignKey, $localKey)
    {
        $qualifiedForeignKey = "$table.$foreignKey";
        $bindingIndex = null;

        //Search for the 'where' value currently linking the pivot table's foreign key to the model's primary key value
        $query->getQuery()->wheres = collect($query->getQuery()->wheres)->map(function ($where, $index) use (
            $qualifiedForeignKey,
            $parent,
            &$bindingIndex
        ) {
            //Update the key value, and note the index so the corresponding binding can also be updated
            if (array_get($where, 'column', '') == $qualifiedForeignKey) {
                $where['value'] = $this->getKey($parent);
                $bindingIndex = $index;
            }
            return $where;
        })->toArray();

        //If a binding index was discovered, updated it to reflect the value of the custom-defined local key
        if (!is_null($bindingIndex)) {
            $bindgings = $query->getQuery()->getBindings();
            $bindgings[$bindingIndex] = $this->getKey($parent);
            $query->getQuery()->setBindings($bindgings);
        }
    }

    /**
     * Get all of the primary keys for an array of models.
     * Overridden so that the call to $value->getKey() is replaced with $this->getKey()
     *
     * @param  array   $models
     * @param  string  $key
     * @return array
     */
    protected function getKeys(array $models, $key = null)
    {
        if ($key) {
            return parent::getKeys($models, $key);
        }
        return array_unique(array_values(array_map(function ($value) use ($key) {
            return $this->getKey($value);
        }, $models)));
    }

    /**
     * If a custom local-key field is defined, don't automatically assume the pivot table's foreign relationship is
     * joined to the model's primary key.  This method is necessary for eager-loading.
     *
     * @param Model $model
     * @return mixed
     */
    protected function getKey(Model $model)
    {
        return $this->localKey ? $model->getAttribute($this->localKey) : $model->getKey();
    }

    /**
     * Set the where clause for the relation query.
     * Overridden so that the call to $this->parent->getKey() is replaced with $this->getKey()
     * This method is not necessary if this class is accessed through the typical flow of a Model::belongsToMany() call.
     * It is necessary if it's instantiated directly.
     *
     * @return $this
     */
    protected function setWhere()
    {
        $foreign = $this->getForeignKey();
        $this->query->where($foreign, '=', $this->getKey($this->parent));
        return $this;
    }
}

接下来,您需要让Model课程实际使用它:

namespace App\Overrides\Traits;

use App\Overrides\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\Relation;

/**
 * Intended for use inside classes that extend Illuminate\Database\Eloquent\Model
 *
 * Class RelationConditions
 * @package App\Overrides\Traits
 */
trait CustomConstraints
{
    /**
     * Intercept the Eloquent Model method and return a custom relation object instead
     *
     * {@inheritdoc}
     */
    public function belongsToMany($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null, $localKey = null)
    {
        //Avoid having to reproduce parent logic here by asking the returned object for its original parameter values
        $base = parent::belongsToMany($related, $table, $foreignKey, $otherKey, $relation);
        //The base action will have already applied the appropriate constraints, so don't re-add them here
        return Relation::noConstraints(function () use ($base, $localKey) {
            //These methods do the same thing, but got renamed
            $foreignKeyName = version_compare(app()->version(), '5.3', '>=')
                ? $base->getQualifiedForeignKeyName()
                : $base->getForeignKey();

            $relatedKeyName = version_compare(app()->version(), '5.3', '>=')
                ? $base->getQualifiedRelatedKeyName()
                : $base->getOtherKey();

            return new BelongsToMany(
                $base->getQuery(),
                $base->getParent(),
                $base->getTable(),
                last(explode('.', $foreignKeyName)),
                last(explode('.', $relatedKeyName)),
                $base->getRelationName(),
                $localKey
            );
        });
    }
}

在模型类中使用此特征,现在您可以添加第6个参数来指定要使用的本地密钥,而不是自动承担主要密钥。