Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Add one-of-many relationship (inner join) #37362

Merged
merged 43 commits into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
d53deaf
added one-of-many to has-one
cbl May 4, 2021
b4a5de1
Apply fixes from StyleCI
cbl May 4, 2021
1d60069
fixed getResults
cbl May 4, 2021
c13cd9a
added query methods to forwardToOneOfManyQuery
cbl May 4, 2021
61af2f4
Apply fixes from StyleCI
cbl May 4, 2021
e76939c
improvements & tests
cbl May 4, 2021
a899dc5
Apply fixes from StyleCI
cbl May 4, 2021
38e021a
use where or having
cbl May 5, 2021
a8218c3
Apply fixes from StyleCI
cbl May 5, 2021
19354b8
join
cbl May 12, 2021
3fd1aff
wip
cbl May 12, 2021
176432e
wip
cbl May 12, 2021
5e0408c
fixes style
cbl May 12, 2021
33ce8a8
updated contract
cbl May 12, 2021
626fb00
multiple aggregastes
cbl May 13, 2021
571db72
Apply fixes from StyleCI
cbl May 13, 2021
af553ac
Merge branch '8.x' into one_of_many-join
taylorotwell May 14, 2021
6acf826
formatting
taylorotwell May 14, 2021
bfe124b
formatting
cbl May 14, 2021
9b8e1f6
Apply fixes from StyleCI
cbl May 14, 2021
2376991
formatting
taylorotwell May 14, 2021
77dcbbc
formatting
taylorotwell May 14, 2021
dff1bf3
rename class
taylorotwell May 14, 2021
47cccf7
add file
taylorotwell May 14, 2021
c38a605
rename array key
taylorotwell May 14, 2021
5ac60a8
add of-many to morph-one
cbl May 14, 2021
2e8097c
Merge branch 'one_of_many-join' of https://github.com/cbl/framework i…
cbl May 14, 2021
a81c9a5
Apply fixes from StyleCI
cbl May 14, 2021
5b5a5c1
fixed pivot test
cbl May 14, 2021
b9c3d98
Apply fixes from StyleCI
cbl May 14, 2021
85c1e2d
fixed return type
cbl May 15, 2021
6e1997f
formatting
taylorotwell May 17, 2021
e32edb6
add shortcut methods
taylorotwell May 17, 2021
159db71
move test
taylorotwell May 17, 2021
d12c20e
multiple columns in shortcut
cbl May 17, 2021
1a88598
Apply fixes from StyleCI
cbl May 17, 2021
bc5334b
add key when missing
cbl May 17, 2021
390ec52
Apply fixes from StyleCI
cbl May 17, 2021
6a8f16f
use collections
taylorotwell May 17, 2021
5ae44e3
fail for invalid aggregates
cbl May 17, 2021
baefe34
Merge branch 'one_of_many-join' of https://github.com/cbl/framework i…
cbl May 17, 2021
d6bf526
Apply fixes from StyleCI
cbl May 17, 2021
3a6ebff
formatting
taylorotwell May 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Illuminate\Contracts\Database\Eloquent;

use Closure;

interface PartialRelation
{
/**
* Indicate that the relation is a partial of a one-to-many relationship.
*
* @param Closure|string|null $column
* @param string|null $relation
* @param string $relation
* @return $this
*/
public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null);

/**
* Determine whether the relationship is a one-of-many relationship.
*
* @return bool
*/
public function isOneOfMany();
}
163 changes: 163 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

namespace Illuminate\Database\Eloquent\Relations\Concerns;

use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;

trait CanBeOneOfMany
{
/**
* Determines whether the relationship is one-of-many.
*
* @var bool
*/
protected $isOneOfMany = false;

/**
* The name of the relationship.
*
* @var string
*/
protected $relationName;

/**
* whether the relation is a partial of a one-to-many relationship.
*
* @param string|array|null $column
* @param string|Closure|null $aggregate
* @param string|null $relation
* @return $this
*/
public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null)
{
$this->isOneOfMany = true;

$this->relationName = $relation ?: $this->guessRelationship();

$keyName = $this->query->getModel()->getKeyName();

$columns = is_string($columns = $column) ? [
$column => $aggregate,
$keyName => $aggregate,
] : $column;

if ($aggregate instanceof Closure) {
$closure = $aggregate;
}

foreach ($columns as $column => $aggregate) {
$subQuery = $this->newSubQuery(
isset($previous) ? $previous['column'] : $this->foreignKey,
$column, $aggregate
);

if (isset($previous)) {
$this->addJoinSub($subQuery, $previous['sub'], $previous['column']);
} elseif (isset($closure)) {
$closure($subQuery);
}

if (array_key_last($columns) == $column) {
$this->addJoinSub($this->query, $subQuery, $column);
}

$previous = [
'sub' => $subQuery,
'column' => $column,
];
}

return $this;
}

/**
* Get a new query for the related model, grouping the query by the given column, often the foreign key of the relationship.
*
* @param string $groupBy
* @param string|null $column
* @param string|null $aggregate
* @return void
cbl marked this conversation as resolved.
Show resolved Hide resolved
*/
protected function newSubQuery($groupBy, $column = null, $aggregate = null)
{
$subQuery = $this->query->getModel()
->newQuery()
->groupBy($this->qualifyRelatedColumn($groupBy));

if (! is_null($column)) {
$subQuery->selectRaw($aggregate.'('.$column.') as '.$column.', '.$this->foreignKey);
}

return $subQuery;
}

/**
* Add the join subquery to the given query on the given column and the relationship's foreign key.
*
* @param \Illuminate\Database\Eloquent\Builder $parent
* @param \Illuminate\Database\Eloquent\Builder $subQuery
* @param string $on
* @return void
*/
protected function addJoinSub(Builder $parent, Builder $subQuery, $on)
{
$parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) {
$join->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on))
->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey));
});
}

/**
* Get the qualified column name for the one-of-many relationship using the subselect join query's alias.
*
* @param string $column
* @return string
*/
public function qualifySubSelectColumn($column)
{
return $this->getRelationName().'.'.last(explode('.', $column));
}

/**
* Qualify related column using the related table name if it is not already qualified.
*
* @param string $column
* @return string
*/
protected function qualifyRelatedColumn($column)
{
return Str::contains($column, '.') ? $column : $this->query->getModel()->getTable().'.'.$column;
}

/**
* Guess the "hasOne" relationship's name via backtrace.
*
* @return string
*/
protected function guessRelationship()
{
return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function'];
}

/**
* Determine whether the relationship is a one-of-many relationship.
*
* @return bool
*/
public function isOneOfMany()
{
return $this->isOneOfMany;
}

/**
* Get the name of the relationship.
*
* @return string
*/
public function getRelationName()
{
return $this->relationName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Illuminate\Database\Eloquent\Relations\Concerns;

use Illuminate\Contracts\Database\Eloquent\PartialRelation;
use Illuminate\Database\Eloquent\Model;

trait ComparesRelatedModels
Expand All @@ -17,7 +18,8 @@ public function is($model)
return ! is_null($model) &&
$this->compareKeys($this->getParentKey(), $this->getRelatedKeyFrom($model)) &&
$this->related->getTable() === $model->getTable() &&
$this->related->getConnectionName() === $model->getConnectionName();
$this->related->getConnectionName() === $model->getConnectionName() &&
$this->compareOneOfMany($model);
}

/**
Expand Down Expand Up @@ -65,4 +67,25 @@ protected function compareKeys($parentKey, $relatedKey)

return $parentKey === $relatedKey;
}

/**
* Determine if the given model is the correct relationship model.
cbl marked this conversation as resolved.
Show resolved Hide resolved
*
* @param \Illuminate\Database\Eloquent\Model|null $model
* @return bool
*/
protected function compareOneOfMany($model)
{
if (! $this instanceof PartialRelation) {
return true;
}

if (! $this->isOneOfMany()) {
return true;
}

return $this->query
->whereKey($model->getKey())
->exists();
}
}
30 changes: 28 additions & 2 deletions src/Illuminate/Database/Eloquent/Relations/HasOne.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

namespace Illuminate\Database\Eloquent\Relations;

use Illuminate\Contracts\Database\Eloquent\PartialRelation;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany;
use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels;
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels;

class HasOne extends HasOneOrMany
class HasOne extends HasOneOrMany implements PartialRelation
{
use ComparesRelatedModels, SupportsDefaultModels;
use CanBeOneOfMany, ComparesRelatedModels, SupportsDefaultModels;

/**
* Get the results of the relationship.
Expand Down Expand Up @@ -77,4 +80,27 @@ protected function getRelatedKeyFrom(Model $model)
{
return $model->getAttribute($this->getForeignKeyName());
}

/**
* Add the constraints for an internal relationship existence query.
*
* Essentially, these queries compare on column names like "whereColumn".
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parentQuery
* @param array|mixed $columns
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
{
if (! $this->isOneOfMany()) {
return parent::getRelationExistenceQuery($query, $parentQuery, $columns);
}

$query->getQuery()->joins = $this->query->getQuery()->joins;

return $query->select($columns)->whereColumn(
$this->getQualifiedParentKeyName(), '=', $this->getExistenceCompareKey()
);
}
}
Loading