Skip to content

Eloquent: 관계

소개

데이터베이스 테이블은 종종 서로 관련되어 있습니다. 예를 들어, 블로그 게시물은 여러 개의 댓글을 가질 수 있고 주문은 해당 주문을 한 사용자와 관련될 수 있습니다. Eloquent는 이러한 관계를 쉽게 관리하고 사용할 수 있도록 지원하며, 다양한 일반적인 관계를 지원합니다.

관계 정의

Eloquent 관계는 Eloquent 모델 클래스에 대한 메서드로 정의됩니다. 관계는 또한 강력한 쿼리 빌더 역할을 하므로, 관계를 메서드로 정의하면 강력한 메서드 체이닝과 쿼리 기능을 제공합니다. 예를 들어, 이 posts 관계에 추가적인 쿼리 제약 조건을 체이닝할 수 있습니다:

$user->posts()->where('active', 1)->get();

하지만, 관계 사용에 대해 더 깊이 들어가기 전에 Eloquent에서 지원하는 각 관계 유형을 정의하는 방법을 배워보겠습니다.

일대일 / Has One

일대일 관계는 매우 기본적인 유형의 데이터베이스 관계입니다. 예를 들어, User 모델은 하나의 Phone 모델과 연결될 수 있습니다. 이 관계를 정의하기 위해 User 모델에 phone 메서드를 배치합니다. phone 메서드는 hasOne 메서드를 호출하고 그 결과를 반환해야 합니다. hasOne 메서드는 모델의 Illuminate\Database\Eloquent\Model 기본 클래스를 통해 모델에서 사용할 수 있습니다:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
 
class User extends Model
{
/**
* 사용자와 연결된 전화 번호를 가져옵니다.
*/
public function phone(): HasOne
{
return $this->hasOne(Phone::class);
}
}

hasOne 메서드에 전달된 첫 번째 인수는 관련 모델 클래스의 이름입니다. 관계가 정의되면 Eloquent의 동적 속성을 사용하여 관련 레코드를 검색할 수 있습니다. 동적 속성을 사용하면 모델에 정의된 속성인 것처럼 관계 메서드에 접근할 수 있습니다:

$phone = User::find(1)->phone;

Eloquent는 상위 모델 이름을 기반으로 관계의 외래 키를 결정합니다. 이 경우, Phone 모델은 자동으로 user_id 외래 키를 갖는다고 가정됩니다. 이 규칙을 재정의하려면 hasOne 메서드에 두 번째 인수를 전달할 수 있습니다:

return $this->hasOne(Phone::class, 'foreign_key');

또한, Eloquent는 외래 키가 상위 항목의 기본 키 열과 일치하는 값을 가져야 한다고 가정합니다. 즉, Eloquent는 Phone 레코드의 user_id 열에서 사용자의 id 열 값을 찾습니다. 관계에서 id 또는 모델의 $primaryKey 속성 이외의 기본 키 값을 사용하려는 경우 hasOne 메서드에 세 번째 인수를 전달할 수 있습니다:

return $this->hasOne(Phone::class, 'foreign_key', 'local_key');

관계의 역방향 정의

이제 User 모델에서 Phone 모델에 접근할 수 있습니다. 다음으로, 전화기를 소유한 사용자에게 접근할 수 있도록 Phone 모델에 관계를 정의해 보겠습니다. belongsTo 메서드를 사용하여 hasOne 관계의 역방향을 정의할 수 있습니다:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Phone extends Model
{
/**
* 전화기를 소유한 사용자를 가져옵니다.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

user 메서드를 호출할 때, Eloquent는 Phone 모델의 user_id 열과 일치하는 id를 가진 User 모델을 찾으려고 시도합니다.

Eloquent는 관계 메서드의 이름을 검사하고 메서드 이름에 _id를 접미사로 추가하여 외래 키 이름을 결정합니다. 따라서 이 경우 Eloquent는 Phone 모델에 user_id 열이 있다고 가정합니다. 그러나 Phone 모델의 외래 키가 user_id가 아닌 경우 belongsTo 메서드에 두 번째 인수로 사용자 정의 키 이름을 전달할 수 있습니다:

/**
* 전화기를 소유한 사용자를 가져옵니다.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key');
}

상위 모델이 id를 기본 키로 사용하지 않거나 다른 열을 사용하여 연결된 모델을 찾으려는 경우 belongsTo 메서드에 세 번째 인수를 전달하여 상위 테이블의 사용자 정의 키를 지정할 수 있습니다:

/**
* 전화기를 소유한 사용자를 가져옵니다.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}

일대다 / Has Many

일대다 관계는 단일 모델이 하나 이상의 하위 모델의 상위 항목인 관계를 정의하는 데 사용됩니다. 예를 들어, 블로그 게시물은 무한한 수의 댓글을 가질 수 있습니다. 다른 모든 Eloquent 관계와 마찬가지로 일대다 관계는 Eloquent 모델에서 메서드를 정의하여 정의됩니다:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Post extends Model
{
/**
* 블로그 게시물에 대한 댓글을 가져옵니다.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}

Eloquent는 Comment 모델에 대한 적절한 외래 키 열을 자동으로 결정합니다. 규칙에 따라 Eloquent는 상위 모델의 "스네이크 케이스" 이름을 가져와 _id를 접미사로 추가합니다. 따라서 이 예에서 Eloquent는 Comment 모델의 외래 키 열이 post_id라고 가정합니다.

관계 메서드가 정의되면 comments 속성에 접근하여 관련 댓글의 컬렉션에 접근할 수 있습니다. Eloquent는 "동적 관계 속성"을 제공하므로 모델에 속성으로 정의된 것처럼 관계 메서드에 접근할 수 있습니다.

use App\Models\Post;
 
$comments = Post::find(1)->comments;
 
foreach ($comments as $comment) {
// ...
}

모든 관계는 쿼리 빌더 역할도 하기 때문에 comments 메서드를 호출하고 쿼리에 조건을 계속 체이닝하여 관계 쿼리에 추가 제약 조건을 추가할 수 있습니다.

$comment = Post::find(1)->comments()
->where('title', 'foo')
->first();

hasOne 메서드와 마찬가지로 hasMany 메서드에 추가 인수를 전달하여 외래 키와 로컬 키를 재정의할 수도 있습니다:

return $this->hasMany(Comment::class, 'foreign_key');
 
return $this->hasMany(Comment::class, 'foreign_key', 'local_key');

하위 항목에 대한 상위 모델 자동 수화

Eloquent 즉시 로딩을 활용하는 경우에도 하위 모델을 반복하면서 하위 모델에서 상위 모델에 접근하려고 하면 "N + 1" 쿼리 문제가 발생할 수 있습니다.

$posts = Post::with('comments')->get();
 
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->post->title;
}
}

위 예시에서는 "N + 1" 쿼리 문제가 발생했습니다. Post 모델에 대해 댓글이 즉시 로드되었음에도 불구하고, Eloquent는 각 자식 Comment 모델에 대해 자동으로 부모 Post를 채우지 않기 때문입니다.

Eloquent가 자동으로 자식 모델에 부모 모델을 채우도록 하려면 hasMany 관계를 정의할 때 chaperone 메서드를 호출하면 됩니다.

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Post extends Model
{
/**
* 블로그 게시물의 댓글을 가져옵니다.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class)->chaperone();
}
}

또는, 런타임에 자동 부모 채우기를 선택하려면 관계를 즉시 로드할 때 chaperone 모델을 호출하면 됩니다.

use App\Models\Post;
 
$posts = Post::with([
'comments' => fn ($comments) => $comments->chaperone(),
])->get();

일대다 (역방향) / Belongs To

이제 게시물의 모든 댓글에 접근할 수 있으므로, 댓글이 부모 게시물에 접근할 수 있도록 관계를 정의해 보겠습니다. hasMany 관계의 역방향 관계를 정의하려면, 자식 모델에서 belongsTo 메서드를 호출하는 관계 메서드를 정의합니다.

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Comment extends Model
{
/**
* 댓글을 소유한 게시물을 가져옵니다.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}

관계가 정의되면, post "동적 관계 속성"에 접근하여 댓글의 부모 게시물을 검색할 수 있습니다.

use App\Models\Comment;
 
$comment = Comment::find(1);
 
return $comment->post->title;

위 예에서, Eloquent는 Comment 모델의 post_id 컬럼과 일치하는 id를 가진 Post 모델을 찾으려고 시도합니다.

Eloquent는 관계 메서드의 이름을 검사하고 메서드 이름에 _를 붙인 후 부모 모델의 기본 키 컬럼 이름을 붙여 기본 외래 키 이름을 결정합니다. 따라서 이 예에서 Eloquent는 comments 테이블의 Post 모델의 외래 키가 post_id라고 가정합니다.

그러나 관계의 외래 키가 이러한 규칙을 따르지 않는 경우, belongsTo 메서드의 두 번째 인수로 사용자 정의 외래 키 이름을 전달할 수 있습니다.

/**
* 댓글을 소유한 게시물을 가져옵니다.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key');
}

부모 모델이 id를 기본 키로 사용하지 않거나 다른 컬럼을 사용하여 연결된 모델을 찾으려면, belongsTo 메서드에 세 번째 인수로 부모 테이블의 사용자 정의 키를 지정할 수 있습니다.

/**
* 댓글을 소유한 게시물을 가져옵니다.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}

기본 모델

belongsTo, hasOne, hasOneThrough, 및 morphOne 관계는 주어진 관계가 null인 경우 반환될 기본 모델을 정의할 수 있도록 합니다. 이 패턴은 종종 Null Object 패턴이라고 하며 코드에서 조건부 검사를 제거하는 데 도움이 될 수 있습니다. 다음 예에서, user 관계는 Post 모델에 연결된 사용자가 없으면 빈 App\Models\User 모델을 반환합니다.

/**
* 게시물의 작성자를 가져옵니다.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault();
}

기본 모델에 속성을 채우려면, withDefault 메서드에 배열 또는 클로저를 전달할 수 있습니다.

/**
* 게시물의 작성자를 가져옵니다.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault([
'name' => 'Guest Author',
]);
}
 
/**
* 게시물의 작성자를 가져옵니다.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
$user->name = 'Guest Author';
});
}

Belongs To 관계 쿼리

"belongs to" 관계의 자식을 쿼리할 때, where 절을 수동으로 만들어 해당하는 Eloquent 모델을 검색할 수 있습니다.

use App\Models\Post;
 
$posts = Post::where('user_id', $user->id)->get();

그러나 주어진 모델에 대한 적절한 관계와 외래 키를 자동으로 결정하는 whereBelongsTo 메서드를 사용하는 것이 더 편리할 수 있습니다.

$posts = Post::whereBelongsTo($user)->get();

또한 whereBelongsTo 메서드에 collection 인스턴스를 제공할 수도 있습니다. 이렇게 하면 Laravel은 컬렉션 내의 부모 모델에 속하는 모델을 검색합니다.

$users = User::where('vip', true)->get();
 
$posts = Post::whereBelongsTo($users)->get();

기본적으로 Laravel은 모델 클래스 이름을 기반으로 주어진 모델과 관련된 관계를 결정하지만, whereBelongsTo 메서드의 두 번째 인수로 관계 이름을 수동으로 지정할 수 있습니다.

$posts = Post::whereBelongsTo($user, 'author')->get();

Has One of Many

때로는 모델에 많은 관련 모델이 있을 수 있지만, 관계의 "최신" 또는 "가장 오래된" 관련 모델을 쉽게 검색하고 싶을 수 있습니다. 예를 들어, User 모델이 많은 Order 모델과 관련되어 있지만, 사용자가 가장 최근에 주문한 내역을 편리하게 상호 작용하는 방법을 정의하고 싶을 수 있습니다. hasOne 관계 유형과 ofMany 메서드를 결합하여 이를 달성할 수 있습니다.

/**
* 사용자의 가장 최근 주문을 가져옵니다.
*/
public function latestOrder(): HasOne
{
return $this->hasOne(Order::class)->latestOfMany();
}

마찬가지로, 관계의 "가장 오래된" 또는 첫 번째 관련 모델을 검색하는 메서드를 정의할 수 있습니다.

/**
* 사용자의 가장 오래된 주문을 가져옵니다.
*/
public function oldestOrder(): HasOne
{
return $this->hasOne(Order::class)->oldestOfMany();
}

기본적으로 latestOfManyoldestOfMany 메서드는 정렬 가능해야 하는 모델의 기본 키를 기준으로 가장 최신 또는 가장 오래된 관련 모델을 검색합니다. 그러나 때로는 다른 정렬 기준을 사용하여 더 큰 관계에서 단일 모델을 검색할 수 있습니다.

예를 들어, ofMany 메서드를 사용하면 사용자의 가장 비싼 주문을 검색할 수 있습니다. ofMany 메서드는 정렬 가능한 열을 첫 번째 인수로 받아들이고 관련 모델을 쿼리할 때 적용할 집계 함수(min 또는 max)를 두 번째 인수로 받아들입니다.

/**
* 사용자의 가장 큰 주문을 가져옵니다.
*/
public function largestOrder(): HasOne
{
return $this->hasOne(Order::class)->ofMany('price', 'max');
}
exclamation

PostgreSQL은 UUID 열에 대해 MAX 함수 실행을 지원하지 않으므로 현재 PostgreSQL UUID 열과 함께 one-of-many 관계를 사용할 수 없습니다.

"Many" 관계를 Has One 관계로 변환하기

종종 latestOfMany, oldestOfMany 또는 ofMany 메서드를 사용하여 단일 모델을 검색할 때 동일한 모델에 대해 이미 "has many" 관계가 정의되어 있습니다. 편의를 위해 Laravel을 사용하면 관계에 one 메서드를 호출하여 이 관계를 "has one" 관계로 쉽게 변환할 수 있습니다.

/**
* 사용자의 주문을 가져옵니다.
*/
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
 
/**
* 사용자의 가장 큰 주문을 가져옵니다.
*/
public function largestOrder(): HasOne
{
return $this->orders()->one()->ofMany('price', 'max');
}

고급 "Has One of Many" 관계

더욱 발전된 "has one of many" 관계를 구성할 수 있습니다. 예를 들어, Product 모델은 새로운 가격이 게시된 후에도 시스템에 유지되는 여러 개의 연결된 Price 모델을 가질 수 있습니다. 또한, 제품에 대한 새로운 가격 데이터는 published_at 열을 통해 미래 날짜에 효력이 발생하도록 미리 게시될 수 있습니다.

요약하자면, 게시일이 미래가 아닌 최신 게시 가격을 검색해야 합니다. 또한, 두 가격의 게시일이 같으면 ID가 가장 큰 가격을 선호합니다. 이를 달성하려면 ofMany 메서드에 최신 가격을 결정하는 정렬 가능한 열을 포함하는 배열을 전달해야 합니다. 또한, 클로저가 ofMany 메서드의 두 번째 인수로 제공됩니다. 이 클로저는 관계 쿼리에 추가 게시일 제약 조건을 추가하는 역할을 담당합니다.

/**
* 제품의 현재 가격을 가져옵니다.
*/
public function currentPricing(): HasOne
{
return $this->hasOne(Price::class)->ofMany([
'published_at' => 'max',
'id' => 'max',
], function (Builder $query) {
$query->where('published_at', '<', now());
});
}

Has One Through

"has-one-through" 관계는 다른 모델과의 일대일 관계를 정의합니다. 하지만, 이 관계는 선언하는 모델이 세 번째 모델을 통해 다른 모델의 인스턴스 하나와 일치할 수 있음을 나타냅니다.

예를 들어, 차량 수리점 애플리케이션에서 각 Mechanic 모델은 하나의 Car 모델과 연결될 수 있고, 각 Car 모델은 하나의 Owner 모델과 연결될 수 있습니다. 정비공과 차주는 데이터베이스 내에서 직접적인 관계가 없지만, 정비공은 Car 모델을 통해 차주에 접근할 수 있습니다. 이 관계를 정의하는 데 필요한 테이블을 살펴보겠습니다.

mechanics
id - integer
name - string
 
cars
id - integer
model - string
mechanic_id - integer
 
owners
id - integer
name - string
car_id - integer

이제 관계에 대한 테이블 구조를 검토했으니, Mechanic 모델에서 관계를 정의해 보겠습니다.

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
 
class Mechanic extends Model
{
/**
* Get the car's owner.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(Owner::class, Car::class);
}
}

hasOneThrough 메서드에 전달되는 첫 번째 인수는 접근하려는 최종 모델의 이름이고, 두 번째 인수는 중간 모델의 이름입니다.

또는, 관계에 관련된 모든 모델에 관련 관계가 이미 정의되어 있는 경우 through 메서드를 호출하고 해당 관계의 이름을 제공하여 "has-one-through" 관계를 유창하게 정의할 수 있습니다. 예를 들어, Mechanic 모델에 cars 관계가 있고 Car 모델에 owner 관계가 있는 경우 다음과 같이 정비공과 차주를 연결하는 "has-one-through" 관계를 정의할 수 있습니다.

// 문자열 기반 구문...
return $this->through('cars')->has('owner');
 
// 동적 구문...
return $this->throughCars()->hasOwner();

키 규칙

관계 쿼리를 수행할 때 일반적인 Eloquent 외래 키 규칙이 사용됩니다. 관계의 키를 사용자 정의하려면 hasOneThrough 메서드에 세 번째 및 네 번째 인수로 전달하면 됩니다. 세 번째 인수는 중간 모델의 외래 키 이름입니다. 네 번째 인수는 최종 모델의 외래 키 이름입니다. 다섯 번째 인수는 로컬 키이고, 여섯 번째 인수는 중간 모델의 로컬 키입니다.

class Mechanic extends Model
{
/**
* 차량의 소유자를 가져옵니다.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(
Owner::class,
Car::class,
'mechanic_id', // cars 테이블의 외래 키...
'car_id', // owners 테이블의 외래 키...
'id', // mechanics 테이블의 로컬 키...
'id' // cars 테이블의 로컬 키...
);
}
}

또는 앞서 설명한 것처럼, 관계에 관련된 모든 모델에 관련 관계가 이미 정의되어 있는 경우, through 메서드를 호출하고 해당 관계의 이름을 제공하여 "has-one-through" 관계를 유창하게 정의할 수 있습니다. 이 접근 방식은 기존 관계에 이미 정의된 키 규칙을 재사용하는 장점을 제공합니다.

// 문자열 기반 구문...
return $this->through('cars')->has('owner');
 
// 동적 구문...
return $this->throughCars()->hasOwner();

Has Many Through

"has-many-through" 관계는 중간 관계를 통해 먼 관계에 접근하는 편리한 방법을 제공합니다. 예를 들어, Laravel Vapor와 같은 배포 플랫폼을 구축한다고 가정해 보겠습니다. Project 모델은 중간 Environment 모델을 통해 여러 Deployment 모델에 접근할 수 있습니다. 이 예시를 사용하면 특정 프로젝트에 대한 모든 배포를 쉽게 수집할 수 있습니다. 이 관계를 정의하는 데 필요한 테이블을 살펴보겠습니다.

projects
id - integer
name - string
 
environments
id - integer
project_id - integer
name - string
 
deployments
id - integer
environment_id - integer
commit_hash - string

이제 관계에 대한 테이블 구조를 살펴보았으므로 Project 모델에 관계를 정의해 보겠습니다.

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
 
class Project extends Model
{
/**
* 프로젝트의 모든 배포를 가져옵니다.
*/
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(Deployment::class, Environment::class);
}
}

hasManyThrough 메서드에 전달된 첫 번째 인수는 접근하려는 최종 모델의 이름이고, 두 번째 인수는 중간 모델의 이름입니다.

또는 관계에 관련된 모든 모델에 관련 관계가 이미 정의되어 있는 경우 through 메서드를 호출하고 해당 관계 이름을 제공하여 "has-many-through" 관계를 유연하게 정의할 수 있습니다. 예를 들어, Project 모델에 environments 관계가 있고 Environment 모델에 deployments 관계가 있는 경우 다음과 같이 프로젝트와 배포를 연결하는 "has-many-through" 관계를 정의할 수 있습니다.

// 문자열 기반 구문...
return $this->through('environments')->has('deployments');
 
// 동적 구문...
return $this->throughEnvironments()->hasDeployments();

Deployment 모델의 테이블에는 project_id 컬럼이 없지만, hasManyThrough 관계를 통해 $project->deployments를 통해 프로젝트의 배포에 접근할 수 있습니다. 이러한 모델들을 검색하기 위해, Eloquent는 중간 모델인 Environment 모델의 테이블에서 project_id 컬럼을 조사합니다. 관련 환경 ID를 찾은 후, 해당 ID를 사용하여 Deployment 모델의 테이블을 쿼리합니다.

키 규칙

관계 쿼리를 수행할 때 일반적인 Eloquent 외래 키 규칙이 사용됩니다. 관계의 키를 사용자 정의하고 싶다면, hasManyThrough 메서드에 세 번째와 네 번째 인수로 전달할 수 있습니다. 세 번째 인수는 중간 모델의 외래 키 이름입니다. 네 번째 인수는 최종 모델의 외래 키 이름입니다. 다섯 번째 인수는 로컬 키이며, 여섯 번째 인수는 중간 모델의 로컬 키입니다.

class Project extends Model
{
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(
Deployment::class,
Environment::class,
'project_id', // environments 테이블의 외래 키...
'environment_id', // deployments 테이블의 외래 키...
'id', // projects 테이블의 로컬 키...
'id' // environments 테이블의 로컬 키...
);
}
}

또는 앞서 논의한 것처럼, 관계에 관련된 모든 모델에 관련 관계가 이미 정의되어 있는 경우, through 메서드를 호출하고 해당 관계 이름을 제공하여 "has-many-through" 관계를 유창하게 정의할 수 있습니다. 이 방법은 기존 관계에 이미 정의된 키 규칙을 재사용하는 이점을 제공합니다.

// 문자열 기반 구문...
return $this->through('environments')->has('deployments');
 
// 동적 구문...
return $this->throughEnvironments()->hasDeployments();

다대다 관계

다대다 관계는 hasOnehasMany 관계보다 약간 더 복잡합니다. 다대다 관계의 예시는 한 사용자가 여러 역할을 가질 수 있고 해당 역할이 애플리케이션의 다른 사용자들과 공유되는 경우입니다. 예를 들어, 한 사용자에게 "작성자" 및 "편집자" 역할이 할당될 수 있지만, 이러한 역할은 다른 사용자에게도 할당될 수 있습니다. 따라서 사용자는 여러 역할을 가지고, 역할은 여러 사용자를 가집니다.

테이블 구조

이 관계를 정의하려면 세 개의 데이터베이스 테이블, 즉 users, roles, 그리고 role_user가 필요합니다. role_user 테이블은 관련 모델 이름의 알파벳 순서에서 파생되며 user_idrole_id 열을 포함합니다. 이 테이블은 사용자와 역할을 연결하는 중간 테이블로 사용됩니다.

역할은 여러 사용자에게 속할 수 있으므로 roles 테이블에 user_id 열을 단순히 추가할 수는 없습니다. 이는 역할이 단일 사용자에게만 속할 수 있음을 의미합니다. 여러 사용자에게 역할이 할당되는 것을 지원하기 위해 role_user 테이블이 필요합니다. 관계의 테이블 구조를 다음과 같이 요약할 수 있습니다:

users
id - 정수
name - 문자열
 
roles
id - 정수
name - 문자열
 
role_user
user_id - 정수
role_id - 정수

모델 구조

다대다 관계는 belongsToMany 메서드의 결과를 반환하는 메서드를 작성하여 정의됩니다. belongsToMany 메서드는 애플리케이션의 모든 Eloquent 모델에서 사용되는 기본 클래스인 Illuminate\Database\Eloquent\Model 클래스에서 제공합니다. 예를 들어, User 모델에 roles 메서드를 정의해 보겠습니다. 이 메서드에 전달되는 첫 번째 인자는 관련된 모델 클래스의 이름입니다.

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class User extends Model
{
/**
* 사용자에 속하는 역할들.
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
}

관계가 정의되면, roles 동적 관계 속성을 사용하여 사용자의 역할에 접근할 수 있습니다:

use App\Models\User;
 
$user = User::find(1);
 
foreach ($user->roles as $role) {
// ...
}

모든 관계는 쿼리 빌더 역할도 하므로, roles 메서드를 호출하고 쿼리에 조건을 계속 연결하여 관계 쿼리에 추가 제약을 추가할 수 있습니다:

$roles = User::find(1)->roles()->orderBy('name')->get();

관계의 중간 테이블 이름을 결정하기 위해 Eloquent는 관련된 두 모델 이름을 알파벳 순서로 결합합니다. 그러나 이 규칙을 자유롭게 재정의할 수 있습니다. belongsToMany 메서드에 두 번째 인자를 전달하여 그렇게 할 수 있습니다:

return $this->belongsToMany(Role::class, 'role_user');

중간 테이블 이름을 사용자 정의하는 것 외에도, belongsToMany 메서드에 추가 인수를 전달하여 테이블의 키 열 이름도 사용자 정의할 수 있습니다. 세 번째 인수는 관계를 정의하는 모델의 외래 키 이름이고, 네 번째 인수는 조인하려는 모델의 외래 키 이름입니다:

return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');

관계의 역방향 정의

다대다 관계의 "역방향"을 정의하려면 belongsToMany 메서드의 결과를 반환하는 메서드를 관련된 모델에 정의해야 합니다. 사용자/역할 예시를 완료하기 위해 Role 모델에 users 메서드를 정의해 보겠습니다:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class Role extends Model
{
/**
* 역할에 속하는 사용자들.
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}

보시다시피, App\Models\User 모델을 참조한다는 점을 제외하고 관계는 User 모델과 정확히 동일하게 정의됩니다. belongsToMany 메서드를 재사용하므로 다대다 관계의 "역방향"을 정의할 때 일반적인 테이블 및 키 사용자 정의 옵션을 모두 사용할 수 있습니다.

중간 테이블 열 검색

이미 배운 대로, 다대다 관계를 사용하려면 중간 테이블이 있어야 합니다. Eloquent는 이 테이블과 상호 작용하는 데 매우 유용한 방법을 제공합니다. 예를 들어, User 모델이 관련된 여러 Role 모델을 가지고 있다고 가정해 보겠습니다. 이 관계에 접근한 후에는 모델의 pivot 속성을 사용하여 중간 테이블에 접근할 수 있습니다:

use App\Models\User;
 
$user = User::find(1);
 
foreach ($user->roles as $role) {
echo $role->pivot->created_at;
}

가져오는 각 Role 모델에는 pivot 속성이 자동으로 할당됩니다. 이 속성은 중간 테이블을 나타내는 모델을 포함합니다.

기본적으로 모델 키만 pivot 모델에 존재합니다. 중간 테이블에 추가 속성이 있는 경우, 관계를 정의할 때 해당 속성을 지정해야 합니다:

return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');

중간 테이블에 Eloquent에서 자동으로 유지 관리하는 created_atupdated_at 타임스탬프가 있도록 하려면 관계를 정의할 때 withTimestamps 메서드를 호출합니다:

return $this->belongsToMany(Role::class)->withTimestamps();
exclamation

Eloquent에서 자동으로 유지 관리하는 타임스탬프를 사용하는 중간 테이블에는 created_atupdated_at 타임스탬프 열이 모두 있어야 합니다.

pivot 속성 이름 사용자 정의

앞에서 언급했듯이 중간 테이블의 속성은 pivot 속성을 통해 모델에서 접근할 수 있습니다. 그러나 애플리케이션 내에서 해당 속성의 목적을 더 잘 반영하도록 이 속성의 이름을 자유롭게 사용자 정의할 수 있습니다.

예를 들어, 애플리케이션에 팟캐스트를 구독할 수 있는 사용자가 포함되어 있다면, 사용자와 팟캐스트 사이에 다대다 관계가 있을 가능성이 높습니다. 이 경우, 중간 테이블 속성의 이름을 pivot 대신 subscription으로 변경하고 싶을 수 있습니다. 이는 관계를 정의할 때 as 메서드를 사용하여 수행할 수 있습니다:

return $this->belongsToMany(Podcast::class)
->as('subscription')
->withTimestamps();

사용자 정의 중간 테이블 속성이 지정되면, 사용자 정의된 이름을 사용하여 중간 테이블 데이터에 접근할 수 있습니다:

$users = User::with('podcasts')->get();
 
foreach ($users->flatMap->podcasts as $podcast) {
echo $podcast->subscription->created_at;
}

중간 테이블 열을 통한 쿼리 필터링

관계를 정의할 때 wherePivot, wherePivotIn, wherePivotNotIn, wherePivotBetween, wherePivotNotBetween, wherePivotNullwherePivotNotNull 메서드를 사용하여 belongsToMany 관계 쿼리에서 반환된 결과를 필터링할 수도 있습니다:

return $this->belongsToMany(Role::class)
->wherePivot('approved', 1);
 
return $this->belongsToMany(Role::class)
->wherePivotIn('priority', [1, 2]);
 
return $this->belongsToMany(Role::class)
->wherePivotNotIn('priority', [1, 2]);
 
return $this->belongsToMany(Podcast::class)
->as('subscriptions')
->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
 
return $this->belongsToMany(Podcast::class)
->as('subscriptions')
->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);
 
return $this->belongsToMany(Podcast::class)
->as('subscriptions')
->wherePivotNull('expired_at');
 
return $this->belongsToMany(Podcast::class)
->as('subscriptions')
->wherePivotNotNull('expired_at');

중간 테이블 열을 통한 쿼리 정렬

orderByPivot 메서드를 사용하여 belongsToMany 관계 쿼리에서 반환된 결과를 정렬할 수 있습니다. 다음 예에서는 사용자의 최신 배지를 모두 검색합니다:

return $this->belongsToMany(Badge::class)
->where('rank', 'gold')
->orderByPivot('created_at', 'desc');

사용자 정의 중간 테이블 모델 정의

다대다 관계의 중간 테이블을 나타내는 사용자 정의 모델을 정의하려면 관계를 정의할 때 using 메서드를 호출할 수 있습니다. 사용자 정의 피벗 모델을 사용하면 메서드 및 캐스트와 같은 피벗 모델에 대한 추가 동작을 정의할 수 있습니다.

사용자 정의 다대다 피벗 모델은 Illuminate\Database\Eloquent\Relations\Pivot 클래스를 확장해야 하고, 사용자 정의 다형성 다대다 피벗 모델은 Illuminate\Database\Eloquent\Relations\MorphPivot 클래스를 확장해야 합니다. 예를 들어, 사용자 정의 RoleUser 피벗 모델을 사용하는 Role 모델을 정의할 수 있습니다:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class Role extends Model
{
/**
* 역할에 속하는 사용자들.
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)->using(RoleUser::class);
}
}

RoleUser 모델을 정의할 때는 Illuminate\Database\Eloquent\Relations\Pivot 클래스를 확장해야 합니다:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Relations\Pivot;
 
class RoleUser extends Pivot
{
// ...
}
exclamation

피벗 모델은 SoftDeletes 트레이트를 사용할 수 없습니다. 피벗 레코드를 소프트 삭제해야 하는 경우, 피벗 모델을 실제 Eloquent 모델로 변환하는 것을 고려하십시오.

사용자 정의 피벗 모델 및 자동 증가 ID

사용자 정의 피벗 모델을 사용하는 다대다 관계를 정의했고 해당 피벗 모델에 자동 증가 기본 키가 있는 경우, 사용자 정의 피벗 모델 클래스에서 incrementing 속성을 true로 설정했는지 확인해야 합니다.

/**
* ID가 자동으로 증가하는지 여부를 나타냅니다.
*
* @var bool
*/
public $incrementing = true;

다형성 관계

다형성 관계는 자식 모델이 단일 연결을 사용하여 둘 이상의 모델 유형에 속할 수 있도록 합니다. 예를 들어, 사용자가 블로그 게시물과 비디오를 공유할 수 있도록 하는 애플리케이션을 구축한다고 가정해 보겠습니다. 이러한 애플리케이션에서 Comment 모델은 PostVideo 모델 모두에 속할 수 있습니다.

일대일 (다형성)

테이블 구조

일대일 다형성 관계는 일반적인 일대일 관계와 유사합니다. 그러나 자식 모델은 단일 연결을 사용하여 둘 이상의 모델 유형에 속할 수 있습니다. 예를 들어, 블로그 PostUserImage 모델에 대한 다형성 관계를 공유할 수 있습니다. 일대일 다형성 관계를 사용하면 게시물과 사용자에 연결될 수 있는 고유한 이미지의 단일 테이블을 가질 수 있습니다. 먼저 테이블 구조를 살펴보겠습니다:

posts
id - 정수
name - 문자열
 
users
id - 정수
name - 문자열
 
images
id - 정수
url - 문자열
imageable_id - 정수
imageable_type - 문자열

images 테이블의 imageable_idimageable_type 열에 주목하십시오. imageable_id 열에는 게시물 또는 사용자의 ID 값이 포함되고, imageable_type 열에는 부모 모델의 클래스 이름이 포함됩니다. imageable_type 열은 imageable 관계에 접근할 때 반환할 부모 모델의 "유형"을 결정하기 위해 Eloquent에서 사용됩니다. 이 경우, 열에는 App\Models\Post 또는 App\Models\User가 포함됩니다.

모델 구조

다음으로, 이 관계를 구축하는 데 필요한 모델 정의를 살펴보겠습니다:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class Image extends Model
{
/**
* 부모 imageable 모델(사용자 또는 게시물)을 가져옵니다.
*/
public function imageable(): MorphTo
{
return $this->morphTo();
}
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
 
class Post extends Model
{
/**
* 게시물의 이미지를 가져옵니다.
*/
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
 
class User extends Model
{
/**
* 사용자의 이미지를 가져옵니다.
*/
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}

관계 검색

데이터베이스 테이블과 모델이 정의되면, 모델을 통해 관계에 접근할 수 있습니다. 예를 들어, 게시물의 이미지를 검색하려면 image 동적 관계 속성에 접근할 수 있습니다:

use App\Models\Post;
 
$post = Post::find(1);
 
$image = $post->image;

morphTo 호출을 수행하는 메서드의 이름에 접근하여 다형성 모델의 부모를 검색할 수 있습니다. 이 경우, Image 모델의 imageable 메서드입니다. 따라서 해당 메서드를 동적 관계 속성으로 접근합니다:

use App\Models\Image;
 
$image = Image::find(1);
 
$imageable = $image->imageable;

Image 모델의 imageable 관계는 이미지 소유자 모델의 유형에 따라 Post 또는 User 인스턴스를 반환합니다.

키 규칙

필요한 경우 다형성 자식 모델에서 사용하는 "id" 및 "type" 열의 이름을 지정할 수 있습니다. 이렇게 하는 경우 항상 관계의 이름을 morphTo 메서드에 대한 첫 번째 인수로 전달해야 합니다. 일반적으로 이 값은 메서드 이름과 일치해야 하므로 PHP의 __FUNCTION__ 상수를 사용할 수 있습니다:

/**
* 이미지가 속한 모델을 가져옵니다.
*/
public function imageable(): MorphTo
{
return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}

일대다 (다형성)

테이블 구조

일대다 다형성 관계는 일반적인 일대다 관계와 유사합니다. 그러나 자식 모델은 단일 연결을 사용하여 둘 이상의 모델 유형에 속할 수 있습니다. 예를 들어, 애플리케이션 사용자가 게시물과 비디오에 "댓글"을 달 수 있다고 가정해 보겠습니다. 다형성 관계를 사용하면 게시물과 비디오 모두에 대한 댓글을 포함하는 단일 comments 테이블을 사용할 수 있습니다. 먼저 이 관계를 구축하는 데 필요한 테이블 구조를 살펴보겠습니다:

posts
id - 정수
title - 문자열
body - 텍스트
 
videos
id - 정수
title - 문자열
url - 문자열
 
comments
id - 정수
body - 텍스트
commentable_id - 정수
commentable_type - 문자열

모델 구조

다음으로, 이 관계를 구축하는 데 필요한 모델 정의를 살펴보겠습니다:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class Comment extends Model
{
/**
* 부모 commentable 모델(게시물 또는 비디오)을 가져옵니다.
*/
public function commentable(): MorphTo
{
return $this->morphTo();
}
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
 
class Post extends Model
{
/**
* 게시물의 모든 댓글을 가져옵니다.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
 
class Video extends Model
{
/**
* 비디오의 모든 댓글을 가져옵니다.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}

관계 검색

데이터베이스 테이블과 모델이 정의되면 모델의 동적 관계 속성을 통해 관계에 접근할 수 있습니다. 예를 들어, 게시물에 대한 모든 댓글에 접근하려면 comments 동적 속성을 사용할 수 있습니다:

use App\Models\Post;
 
$post = Post::find(1);
 
foreach ($post->comments as $comment) {
// ...
}

morphTo 호출을 수행하는 메서드의 이름에 접근하여 다형성 자식 모델의 부모를 검색할 수도 있습니다. 이 경우, Comment 모델의 commentable 메서드입니다. 따라서 댓글의 부모 모델에 접근하기 위해 해당 메서드를 동적 관계 속성으로 접근합니다:

use App\Models\Comment;
 
$comment = Comment::find(1);
 
$commentable = $comment->commentable;

Comment 모델의 commentable 관계는 댓글의 부모 모델의 유형에 따라 Post 또는 Video 인스턴스를 반환합니다.

자식 모델에서 부모 모델을 자동으로 하이드레이션

Eloquent 즉시 로딩을 활용하는 경우에도 자식 모델을 루프하는 동안 자식 모델에서 부모 모델에 접근하려고 하면 "N + 1" 쿼리 문제가 발생할 수 있습니다.

$posts = Post::with('comments')->get();
 
foreach ($posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->commentable->title;
}
}

위 예시에서 "N + 1" 쿼리 문제가 발생했습니다. 모든 Post 모델에 대해 댓글을 즉시 로드했지만, Eloquent는 각 하위 Comment 모델에 대해 자동으로 상위 Post를 채우지 않기 때문입니다.

Eloquent가 자동으로 상위 모델을 해당 하위 모델에 채우도록 하려면 morphMany 관계를 정의할 때 chaperone 메서드를 호출하면 됩니다:

class Post extends Model
{
/**
* 게시물의 모든 댓글을 가져옵니다.
*/
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable')->chaperone();
}
}

또는, 런타임에 자동 상위 모델 채우기를 선택하려면 관계를 즉시 로드할 때 chaperone 모델을 호출하면 됩니다:

use App\Models\Post;
 
$posts = Post::with([
'comments' => fn ($comments) => $comments->chaperone(),
])->get();

One of Many (다형성)

때로는 모델이 여러 관련 모델을 가질 수 있지만, 관계의 "최신" 또는 "가장 오래된" 관련 모델을 쉽게 검색하고 싶을 수 있습니다. 예를 들어, User 모델은 여러 Image 모델과 관련될 수 있지만, 사용자가 업로드한 가장 최근 이미지를 편리하게 상호 작용하는 방법을 정의하고 싶을 수 있습니다. morphOne 관계 유형과 ofMany 메서드를 함께 사용하여 이를 달성할 수 있습니다:

/**
* 사용자의 가장 최근 이미지를 가져옵니다.
*/
public function latestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}

마찬가지로, 관계의 "가장 오래된" 또는 첫 번째 관련 모델을 검색하는 메서드를 정의할 수 있습니다:

/**
* 사용자의 가장 오래된 이미지를 가져옵니다.
*/
public function oldestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}

기본적으로 latestOfManyoldestOfMany 메서드는 정렬 가능한 모델의 기본 키를 기반으로 최신 또는 가장 오래된 관련 모델을 검색합니다. 하지만 때로는 다른 정렬 기준을 사용하여 더 큰 관계에서 단일 모델을 검색하고 싶을 수 있습니다.

예를 들어, ofMany 메서드를 사용하여 사용자가 가장 "좋아하는" 이미지를 검색할 수 있습니다. ofMany 메서드는 정렬 가능한 열을 첫 번째 인수로 받고 관련 모델을 쿼리할 때 적용할 집계 함수 (min 또는 max)를 두 번째 인수로 받습니다.

/**
* 사용자의 가장 인기 있는 이미지를 가져옵니다.
*/
public function bestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}
lightbulb

더 고급 "one of many" 관계를 구성할 수 있습니다. 자세한 내용은 has one of many 문서를 참조하세요.

다대다 (다형)

테이블 구조

다대다 다형 관계는 "morph one" 및 "morph many" 관계보다 약간 더 복잡합니다. 예를 들어, Post 모델과 Video 모델은 Tag 모델에 대한 다형 관계를 공유할 수 있습니다. 이러한 상황에서 다대다 다형 관계를 사용하면 애플리케이션이 게시물이나 비디오와 연결될 수 있는 고유 태그의 단일 테이블을 가질 수 있습니다. 먼저 이 관계를 구축하는 데 필요한 테이블 구조를 살펴보겠습니다.

posts
id - 정수
name - 문자열
 
videos
id - 정수
name - 문자열
 
tags
id - 정수
name - 문자열
 
taggables
tag_id - 정수
taggable_id - 정수
taggable_type - 문자열
lightbulb

다형 다대다 관계를 시작하기 전에 일반적인 다대다 관계에 대한 문서를 읽어보면 도움이 될 수 있습니다.

모델 구조

다음으로, 모델에서 관계를 정의할 준비가 되었습니다. PostVideo 모델 모두 기본 Eloquent 모델 클래스에서 제공하는 morphToMany 메서드를 호출하는 tags 메서드를 포함합니다.

morphToMany 메서드는 관련 모델의 이름과 "관계 이름"을 허용합니다. 중간 테이블 이름과 포함된 키에 할당한 이름을 기반으로 관계를 "taggable"이라고 부릅니다.

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
 
class Post extends Model
{
/**
* 게시물에 대한 모든 태그를 가져옵니다.
*/
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
}

관계의 역 정의

다음으로, Tag 모델에서 가능한 각 부모 모델에 대한 메서드를 정의해야 합니다. 따라서 이 예에서는 posts 메서드와 videos 메서드를 정의합니다. 이 두 메서드 모두 morphedByMany 메서드의 결과를 반환해야 합니다.

morphedByMany 메서드는 관련 모델의 이름과 "관계 이름"을 허용합니다. 중간 테이블 이름과 포함된 키에 할당한 이름을 기반으로 관계를 "taggable"이라고 부릅니다.

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
 
class Tag extends Model
{
/**
* 이 태그가 할당된 모든 게시물을 가져옵니다.
*/
public function posts(): MorphToMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
 
/**
* 이 태그가 할당된 모든 비디오를 가져옵니다.
*/
public function videos(): MorphToMany
{
return $this->morphedByMany(Video::class, 'taggable');
}
}

관계 검색

데이터베이스 테이블과 모델을 정의했으면 모델을 통해 관계에 액세스할 수 있습니다. 예를 들어 게시물의 모든 태그에 액세스하려면 tags 동적 관계 속성을 사용할 수 있습니다.

use App\Models\Post;
 
$post = Post::find(1);
 
foreach ($post->tags as $tag) {
// ...
}

morphedByMany에 대한 호출을 수행하는 메서드의 이름에 액세스하여 다형 자식 모델에서 다형 관계의 부모를 검색할 수 있습니다. 이 경우, Tag 모델의 posts 또는 videos 메서드입니다.

use App\Models\Tag;
 
$tag = Tag::find(1);
 
foreach ($tag->posts as $post) {
// ...
}
 
foreach ($tag->videos as $video) {
// ...
}

사용자 지정 다형 유형

기본적으로 Laravel은 관련 모델의 "유형"을 저장하기 위해 정규화된 클래스 이름을 사용합니다. 예를 들어, 위에서 언급한 일대다 관계 예에서 Comment 모델이 Post 또는 Video 모델에 속할 수 있는 경우 기본 commentable_type은 각각 App\Models\Post 또는 App\Models\Video입니다. 그러나 이러한 값을 애플리케이션의 내부 구조에서 분리할 수도 있습니다.

예를 들어, 모델 이름을 "유형"으로 사용하는 대신 postvideo와 같은 간단한 문자열을 사용할 수 있습니다. 이렇게 하면 모델의 이름을 변경하더라도 데이터베이스의 다형 "유형" 열 값이 유효하게 유지됩니다.

use Illuminate\Database\Eloquent\Relations\Relation;
 
Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);

App\Providers\AppServiceProvider 클래스의 boot 메서드에서 enforceMorphMap 메서드를 호출하거나 원하는 경우 별도의 서비스 제공자를 만들 수 있습니다.

모델의 getMorphClass 메서드를 사용하여 런타임에 주어진 모델의 morph 별칭을 확인할 수 있습니다. 반대로, Relation::getMorphedModel 메서드를 사용하여 morph 별칭과 연결된 정규화된 클래스 이름을 확인할 수 있습니다.

use Illuminate\Database\Eloquent\Relations\Relation;
 
$alias = $post->getMorphClass();
 
$class = Relation::getMorphedModel($alias);
exclamation

기존 애플리케이션에 "morph 맵"을 추가할 때 데이터베이스에서 정규화된 클래스를 여전히 포함하는 모든 morphable *_type 열 값을 "맵" 이름으로 변환해야 합니다.

동적 관계

resolveRelationUsing 메서드를 사용하여 런타임에 Eloquent 모델 간의 관계를 정의할 수 있습니다. 일반적으로 일반적인 애플리케이션 개발에는 권장되지 않지만 Laravel 패키지를 개발할 때 유용할 수 있습니다.

resolveRelationUsing 메서드는 원하는 관계 이름을 첫 번째 인수로 허용합니다. 메서드에 전달된 두 번째 인수는 모델 인스턴스를 허용하고 유효한 Eloquent 관계 정의를 반환하는 클로저여야 합니다. 일반적으로 서비스 제공자의 부트 메서드 내에서 동적 관계를 구성해야 합니다.

use App\Models\Order;
use App\Models\Customer;
 
Order::resolveRelationUsing('customer', function (Order $orderModel) {
return $orderModel->belongsTo(Customer::class, 'customer_id');
});
exclamation

동적 관계를 정의할 때 항상 Eloquent 관계 메서드에 명시적인 키 이름 인수를 제공하십시오.

관계 쿼리

모든 Eloquent 관계는 메서드를 통해 정의되므로 관련 모델을 로드하기 위한 쿼리를 실제로 실행하지 않고도 해당 메서드를 호출하여 관계의 인스턴스를 얻을 수 있습니다. 또한 모든 유형의 Eloquent 관계는 쿼리 빌더 역할도 하므로 데이터베이스에 대해 SQL 쿼리를 최종적으로 실행하기 전에 관계 쿼리에 제약 조건을 계속 추가할 수 있습니다.

예를 들어, User 모델에 많은 관련 Post 모델이 있는 블로그 애플리케이션을 상상해 보십시오.

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class User extends Model
{
/**
* 사용자에 대한 모든 게시물을 가져옵니다.
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}

다음과 같이 posts 관계를 쿼리하고 관계에 추가 제약 조건을 추가할 수 있습니다.

use App\Models\User;
 
$user = User::find(1);
 
$user->posts()->where('active', 1)->get();

관계에서 Laravel 쿼리 빌더 메서드를 사용할 수 있으므로 사용 가능한 모든 메서드에 대해 알아보려면 쿼리 빌더 문서를 살펴보세요.

관계 뒤에 orWhere 절 연결

위의 예에서 알 수 있듯이 쿼리할 때 관계에 추가 제약 조건을 자유롭게 추가할 수 있습니다. 그러나 orWhere 절이 관계 제약 조건과 동일한 수준에서 논리적으로 그룹화되므로 관계에 orWhere 절을 연결할 때는 주의하십시오.

$user->posts()
->where('active', 1)
->orWhere('votes', '>=', 100)
->get();

위의 예는 다음 SQL을 생성합니다. 보시다시피, or 절은 100표 이상을 가진 모든 게시물을 반환하도록 쿼리에 지시합니다. 쿼리는 더 이상 특정 사용자에게 제한되지 않습니다.

select *
from posts
where user_id = ? and active = 1 or votes >= 100

대부분의 경우, 조건 검사를 괄호로 묶어 논리적 그룹을 사용해야 합니다.

use Illuminate\Database\Eloquent\Builder;
 
$user->posts()
->where(function (Builder $query) {
return $query->where('active', 1)
->orWhere('votes', '>=', 100);
})
->get();

위의 예제는 다음 SQL을 생성합니다. 논리적 그룹화가 제약 조건을 올바르게 그룹화했으며 쿼리가 특정 사용자로 제한되어 있음을 유의하십시오.

select *
from posts
where user_id = ? and (active = 1 or votes >= 100)

관계 메서드 vs 동적 속성

Eloquent 관계 쿼리에 추가적인 제약을 추가할 필요가 없다면, 관계를 마치 속성인 것처럼 접근할 수 있습니다. 예를 들어, UserPost 예제 모델을 계속 사용하면 다음과 같이 사용자의 모든 게시물에 접근할 수 있습니다:

use App\Models\User;
 
$user = User::find(1);
 
foreach ($user->posts as $post) {
// ...
}

동적 관계 속성은 "지연 로딩"을 수행합니다. 즉, 실제로 접근할 때만 관계 데이터를 로드합니다. 이러한 이유로 개발자들은 모델을 로드한 후 접근할 관계를 미리 로드하기 위해 즉시 로딩을 자주 사용합니다. 즉시 로딩은 모델의 관계를 로드하기 위해 실행해야 하는 SQL 쿼리 수를 크게 줄여줍니다.

관계 존재 여부 쿼리

모델 레코드를 검색할 때, 관계의 존재 여부에 따라 결과를 제한하고 싶을 수 있습니다. 예를 들어, 댓글이 하나 이상 있는 모든 블로그 게시물을 검색하고 싶다고 가정해 봅시다. 이렇게 하려면 관계 이름을 hasorHas 메서드에 전달하면 됩니다:

use App\Models\Post;
 
// 댓글이 하나 이상 있는 모든 게시물 검색...
$posts = Post::has('comments')->get();

연산자와 개수 값을 지정하여 쿼리를 더 사용자 정의할 수도 있습니다:

// 댓글이 3개 이상 있는 모든 게시물 검색...
$posts = Post::has('comments', '>=', 3)->get();

중첩된 has 문은 "점" 표기법을 사용하여 구성할 수 있습니다. 예를 들어, 이미지가 하나 이상 있는 댓글이 하나 이상 있는 모든 게시물을 검색할 수 있습니다:

// 이미지가 있는 댓글이 하나 이상 있는 게시물 검색...
$posts = Post::has('comments.images')->get();

더 많은 기능이 필요한 경우, whereHasorWhereHas 메서드를 사용하여 댓글 내용 검사와 같이 has 쿼리에 대한 추가 쿼리 제약을 정의할 수 있습니다:

use Illuminate\Database\Eloquent\Builder;
 
// code%와 같은 단어가 포함된 댓글이 하나 이상 있는 게시물 검색...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();
 
// code%와 같은 단어가 포함된 댓글이 10개 이상 있는 게시물 검색...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
}, '>=', 10)->get();
exclamation

Eloquent는 현재 데이터베이스 간의 관계 존재 여부 쿼리를 지원하지 않습니다. 관계는 동일한 데이터베이스 내에 존재해야 합니다.

인라인 관계 존재 여부 쿼리

관계 쿼리에 연결된 단일하고 간단한 where 조건으로 관계의 존재 여부를 쿼리하려는 경우, whereRelation, orWhereRelation, whereMorphRelationorWhereMorphRelation 메서드를 사용하는 것이 더 편리할 수 있습니다. 예를 들어, 승인되지 않은 댓글이 있는 모든 게시물을 쿼리할 수 있습니다:

use App\Models\Post;
 
$posts = Post::whereRelation('comments', 'is_approved', false)->get();

물론, 쿼리 빌더의 where 메서드 호출과 마찬가지로 연산자를 지정할 수도 있습니다:

$posts = Post::whereRelation(
'comments', 'created_at', '>=', now()->subHour()
)->get();

관계 부재 여부 쿼리

모델 레코드를 검색할 때, 관계의 부재 여부에 따라 결과를 제한하고 싶을 수 있습니다. 예를 들어, 댓글이 없는 모든 블로그 게시물을 검색하고 싶다고 가정해 봅시다. 이렇게 하려면 관계 이름을 doesntHaveorDoesntHave 메서드에 전달하면 됩니다:

use App\Models\Post;
 
$posts = Post::doesntHave('comments')->get();

더 많은 기능이 필요한 경우, whereDoesntHaveorWhereDoesntHave 메서드를 사용하여 댓글 내용 검사와 같이 doesntHave 쿼리에 대한 추가 쿼리 제약을 추가할 수 있습니다:

use Illuminate\Database\Eloquent\Builder;
 
$posts = Post::whereDoesntHave('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();

"점" 표기법을 사용하여 중첩된 관계에 대한 쿼리를 실행할 수 있습니다. 예를 들어, 다음 쿼리는 댓글이 없는 모든 게시물을 검색하지만, 금지되지 않은 작성자의 댓글이 있는 게시물은 결과에 포함됩니다:

use Illuminate\Database\Eloquent\Builder;
 
$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {
$query->where('banned', 0);
})->get();

Morph To 관계 쿼리

"morph to" 관계의 존재 여부를 쿼리하려면 whereHasMorphwhereDoesntHaveMorph 메서드를 사용할 수 있습니다. 이러한 메서드는 관계 이름을 첫 번째 인수로 받습니다. 다음으로, 메서드는 쿼리에 포함하려는 관련 모델의 이름을 받습니다. 마지막으로, 관계 쿼리를 사용자 정의하는 클로저를 제공할 수 있습니다:

use App\Models\Comment;
use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Eloquent\Builder;
 
// title이 code%와 같은 게시물 또는 비디오와 관련된 댓글 검색...
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Video::class],
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();
 
// title이 code%와 같지 않은 게시물과 관련된 댓글 검색...
$comments = Comment::whereDoesntHaveMorph(
'commentable',
Post::class,
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();

관련 다형 모델의 "유형"에 따라 쿼리 제약을 추가해야 할 수도 있습니다. whereHasMorph 메서드에 전달된 클로저는 두 번째 인수로 $type 값을 받을 수 있습니다. 이 인수를 통해 빌드 중인 쿼리의 "유형"을 검사할 수 있습니다:

use Illuminate\Database\Eloquent\Builder;
 
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Video::class],
function (Builder $query, string $type) {
$column = $type === Post::class ? 'content' : 'title';
 
$query->where($column, 'like', 'code%');
}
)->get();

경우에 따라 "morph to" 관계의 부모의 자식을 쿼리해야 할 수도 있습니다. 모델에 대한 적절한 morph 유형 매핑을 자동으로 결정하는 whereMorphedTowhereNotMorphedTo 메서드를 사용하여 이를 수행할 수 있습니다. 이러한 메서드는 첫 번째 인수로 morphTo 관계의 이름을 받고 두 번째 인수로 관련 부모 모델을 받습니다:

$comments = Comment::whereMorphedTo('commentable', $post)
->orWhereMorphedTo('commentable', $video)
->get();

가능한 다형 모델 배열을 전달하는 대신, 와일드카드 값으로 *를 제공할 수 있습니다. 이렇게 하면 Laravel이 데이터베이스에서 가능한 모든 다형 유형을 검색하도록 지시합니다. Laravel은 이 작업을 수행하기 위해 추가 쿼리를 실행합니다:

use Illuminate\Database\Eloquent\Builder;
 
$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {
$query->where('title', 'like', 'foo%');
})->get();

때로는 모델을 실제로 로드하지 않고 주어진 관계에 대한 관련 모델 수를 세고 싶을 수 있습니다. 이를 수행하려면 withCount 메서드를 사용할 수 있습니다. withCount 메서드는 결과 모델에 {관계}_count 속성을 배치합니다:

use App\Models\Post;
 
$posts = Post::withCount('comments')->get();
 
foreach ($posts as $post) {
echo $post->comments_count;
}

withCount 메서드에 배열을 전달하면 여러 관계에 대한 "개수"를 추가하고 쿼리에 추가 제약을 추가할 수 있습니다:

use Illuminate\Database\Eloquent\Builder;
 
$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {
$query->where('content', 'like', 'code%');
}])->get();
 
echo $posts[0]->votes_count;
echo $posts[0]->comments_count;

관계 개수 결과에 별칭을 지정하여 동일한 관계에서 여러 개수를 허용할 수도 있습니다:

use Illuminate\Database\Eloquent\Builder;
 
$posts = Post::withCount([
'comments',
'comments as pending_comments_count' => function (Builder $query) {
$query->where('approved', false);
},
])->get();
 
echo $posts[0]->comments_count;
echo $posts[0]->pending_comments_count;

지연 개수 로딩

loadCount 메서드를 사용하여 상위 모델이 이미 검색된 후에 관계 개수를 로드할 수 있습니다:

$book = Book::first();
 
$book->loadCount('genres');

개수 쿼리에 추가 쿼리 제약을 설정해야 하는 경우, 개수를 세려는 관계를 키로 사용하는 배열을 전달할 수 있습니다. 배열 값은 쿼리 빌더 인스턴스를 받는 클로저여야 합니다:

$book->loadCount(['reviews' => function (Builder $query) {
$query->where('rating', 5);
}])

관계 개수 세기 및 사용자 정의 select 문

withCountselect 문과 결합하는 경우, select 메서드 다음에 withCount를 호출해야 합니다:

$posts = Post::select(['title', 'body'])
->withCount('comments')
->get();

기타 집계 함수

Eloquent는 withCount 메서드 외에도 withMin, withMax, withAvg, withSumwithExists 메서드를 제공합니다. 이러한 메서드는 결과 모델에 {관계}_{함수}_{열} 속성을 배치합니다:

use App\Models\Post;
 
$posts = Post::withSum('comments', 'votes')->get();
 
foreach ($posts as $post) {
echo $post->comments_sum_votes;
}

집계 함수의 결과를 다른 이름으로 접근하고 싶다면 사용자 정의 별칭을 지정할 수 있습니다:

$posts = Post::withSum('comments as total_comments', 'votes')->get();
 
foreach ($posts as $post) {
echo $post->total_comments;
}

loadCount 메서드와 마찬가지로 이러한 메서드의 지연 버전도 사용할 수 있습니다. 이러한 추가 집계 작업은 이미 검색된 Eloquent 모델에서 수행할 수 있습니다:

$post = Post::first();
 
$post->loadSum('comments', 'votes');

이러한 집계 메서드를 select 문과 결합하는 경우, select 메서드 다음에 집계 메서드를 호출해야 합니다:

$posts = Post::select(['title', 'body'])
->withExists('comments')
->get();

"morph to" 관계를 즉시 로드하고 해당 관계에서 반환될 수 있는 다양한 엔터티에 대한 관련 모델 개수를 로드하려면 with 메서드를 morphTo 관계의 morphWithCount 메서드와 함께 활용할 수 있습니다.

이 예제에서는 PhotoPost 모델이 ActivityFeed 모델을 만들 수 있다고 가정해 보겠습니다. ActivityFeed 모델은 주어진 ActivityFeed 인스턴스에 대한 부모 Photo 또는 Post 모델을 검색할 수 있도록 parentable이라는 "morph to" 관계를 정의한다고 가정합니다. 또한 Photo 모델에는 "has many" Tag 모델이 있고 Post 모델에는 "has many" Comment 모델이 있다고 가정합니다.

이제 ActivityFeed 인스턴스를 검색하고 각 ActivityFeed 인스턴스에 대한 parentable 부모 모델을 즉시 로드하려고 한다고 가정해 보겠습니다. 또한 각 부모 사진과 연결된 태그 수와 각 부모 게시물과 연결된 댓글 수를 검색하려고 합니다:

use Illuminate\Database\Eloquent\Relations\MorphTo;
 
$activities = ActivityFeed::with([
'parentable' => function (MorphTo $morphTo) {
$morphTo->morphWithCount([
Photo::class => ['tags'],
Post::class => ['comments'],
]);
}])->get();

지연 개수 로딩

이미 ActivityFeed 모델 세트를 검색했고 이제 활동 피드와 연결된 다양한 parentable 모델에 대한 중첩 관계 개수를 로드하려고 한다고 가정해 보겠습니다. loadMorphCount 메서드를 사용하여 이를 수행할 수 있습니다:

$activities = ActivityFeed::with('parentable')->get();
 
$activities->loadMorphCount('parentable', [
Photo::class => ['tags'],
Post::class => ['comments'],
]);

즉시 로딩

Eloquent 관계를 속성으로 접근할 때, 관련 모델은 "지연 로드"됩니다. 즉, 속성에 처음 접근할 때까지 관계 데이터가 실제로 로드되지 않습니다. 그러나 Eloquent는 상위 모델을 쿼리할 때 관계를 "즉시 로드"할 수 있습니다. 즉시 로딩은 "N + 1" 쿼리 문제를 완화합니다. N + 1 쿼리 문제를 설명하기 위해 Author 모델에 "belongs to"인 Book 모델을 생각해 봅시다:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Book extends Model
{
/**
* 책을 쓴 저자를 가져옵니다.
*/
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
}

이제 모든 책과 그 저자를 검색해 보겠습니다:

use App\Models\Book;
 
$books = Book::all();
 
foreach ($books as $book) {
echo $book->author->name;
}

이 루프는 데이터베이스 테이블 내의 모든 책을 검색하는 하나의 쿼리를 실행한 다음 각 책에 대해 해당 책의 저자를 검색하는 또 다른 쿼리를 실행합니다. 따라서 책이 25권 있다면 위의 코드는 26개의 쿼리를 실행합니다. 하나는 원래 책에 대한 쿼리이고 25개는 각 책의 저자를 검색하는 추가 쿼리입니다.

다행히도 즉시 로딩을 사용하여 이 작업을 단 두 개의 쿼리로 줄일 수 있습니다. 쿼리를 빌드할 때 with 메서드를 사용하여 즉시 로드해야 하는 관계를 지정할 수 있습니다:

$books = Book::with('author')->get();
 
foreach ($books as $book) {
echo $book->author->name;
}

이 작업의 경우, 모든 책을 검색하는 하나의 쿼리와 모든 책에 대한 모든 저자를 검색하는 하나의 쿼리, 총 두 개의 쿼리만 실행됩니다:

-- books 테이블의 모든 열과 모든 행을 선택합니다.
select * from books
 
-- authors 테이블에서 id가 (1, 2, 3, 4, 5, ...) 중 하나인 모든 행과 모든 열을 선택합니다.
select * from authors where id in (1, 2, 3, 4, 5, ...)

여러 관계의 즉시 로딩

때로는 여러 개의 다른 관계를 즉시 로딩해야 할 수도 있습니다. 그렇게 하려면 with 메서드에 관계 배열을 전달하기만 하면 됩니다.

$books = Book::with(['author', 'publisher'])->get();

중첩된 즉시 로딩

관계의 관계를 즉시 로딩하려면 "점" 구문을 사용할 수 있습니다. 예를 들어 책의 모든 저자와 모든 저자의 개인 연락처를 즉시 로드해 보겠습니다.

$books = Book::with('author.contacts')->get();

또는 with 메서드에 중첩된 배열을 제공하여 중첩된 즉시 로드된 관계를 지정할 수 있습니다. 이는 여러 개의 중첩된 관계를 즉시 로드할 때 편리할 수 있습니다.

$books = Book::with([
'author' => [
'contacts',
'publisher',
],
])->get();

중첩된 즉시 로딩 morphTo 관계

morphTo 관계와 해당 관계에서 반환될 수 있는 다양한 엔터티의 중첩된 관계를 즉시 로드하려면 with 메서드를 morphTo 관계의 morphWith 메서드와 함께 사용할 수 있습니다. 이 메서드를 설명하기 위해 다음 모델을 고려해 보겠습니다.

<?php
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class ActivityFeed extends Model
{
/**
* 활동 피드 레코드의 부모를 가져옵니다.
*/
public function parentable(): MorphTo
{
return $this->morphTo();
}
}

이 예에서 Event, Photo, Post 모델이 ActivityFeed 모델을 만들 수 있다고 가정해 보겠습니다. 또한 Event 모델은 Calendar 모델에 속하고, Photo 모델은 Tag 모델과 연결되어 있으며, Post 모델은 Author 모델에 속한다고 가정해 보겠습니다.

이러한 모델 정의와 관계를 사용하여 ActivityFeed 모델 인스턴스를 검색하고 모든 parentable 모델과 해당 중첩된 관계를 즉시 로드할 수 있습니다.

use Illuminate\Database\Eloquent\Relations\MorphTo;
 
$activities = ActivityFeed::query()
->with(['parentable' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);
}])->get();

특정 열 즉시 로딩

검색하는 관계에서 모든 열이 항상 필요한 것은 아닙니다. 이러한 이유로 Eloquent는 검색하려는 관계의 열을 지정할 수 있도록 허용합니다.

$books = Book::with('author:id,name,book_id')->get();
exclamation

이 기능을 사용할 때 검색하려는 열 목록에 항상 id 열과 관련 외래 키 열을 포함해야 합니다.

기본적으로 즉시 로딩

모델을 검색할 때 항상 일부 관계를 로드하고 싶을 수도 있습니다. 이를 위해 모델에 $with 속성을 정의할 수 있습니다.

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Book extends Model
{
/**
* 항상 로드해야 하는 관계.
*
* @var array
*/
protected $with = ['author'];
 
/**
* 책을 쓴 저자를 가져옵니다.
*/
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
 
/**
* 책의 장르를 가져옵니다.
*/
public function genre(): BelongsTo
{
return $this->belongsTo(Genre::class);
}
}

단일 쿼리에 대해 $with 속성에서 항목을 제거하려면 without 메서드를 사용할 수 있습니다.

$books = Book::without('author')->get();

단일 쿼리에 대해 $with 속성 내의 모든 항목을 재정의하려면 withOnly 메서드를 사용할 수 있습니다.

$books = Book::withOnly('genre')->get();

즉시 로드 제한

때로는 관계를 즉시 로드하는 동시에 즉시 로드 쿼리에 대한 추가 쿼리 조건을 지정할 수 있습니다. with 메서드에 관계 배열을 전달하여 이를 수행할 수 있습니다. 여기서 배열 키는 관계 이름이고 배열 값은 즉시 로드 쿼리에 추가 제약 조건을 추가하는 클로저입니다.

use App\Models\User;
use Illuminate\Contracts\Database\Eloquent\Builder;
 
$users = User::with(['posts' => function (Builder $query) {
$query->where('title', 'like', '%code%');
}])->get();

이 예에서 Eloquent는 게시물의 title 열에 code라는 단어가 포함된 게시물만 즉시 로드합니다. 다른 쿼리 빌더 메서드를 호출하여 즉시 로드 작업을 추가로 사용자 지정할 수 있습니다.

$users = User::with(['posts' => function (Builder $query) {
$query->orderBy('created_at', 'desc');
}])->get();

morphTo 관계의 즉시 로드 제한

morphTo 관계를 즉시 로드하는 경우 Eloquent는 각 유형의 관련 모델을 가져오기 위해 여러 쿼리를 실행합니다. MorphTo 관계의 constrain 메서드를 사용하여 이러한 각 쿼리에 추가 제약 조건을 추가할 수 있습니다.

use Illuminate\Database\Eloquent\Relations\MorphTo;
 
$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {
$morphTo->constrain([
Post::class => function ($query) {
$query->whereNull('hidden_at');
},
Video::class => function ($query) {
$query->where('type', 'educational');
},
]);
}])->get();

이 예에서 Eloquent는 숨겨지지 않은 게시물과 type 값이 "educational"인 비디오만 즉시 로드합니다.

관계 존재를 사용하여 즉시 로드 제한

때로는 동일한 조건에 따라 관계를 로드하는 동시에 관계의 존재를 확인해야 할 수도 있습니다. 예를 들어, 지정된 쿼리 조건과 일치하는 하위 Post 모델이 있는 User 모델만 검색하고 일치하는 게시물도 즉시 로드할 수 있습니다. withWhereHas 메서드를 사용하여 이를 수행할 수 있습니다.

use App\Models\User;
 
$users = User::withWhereHas('posts', function ($query) {
$query->where('featured', true);
})->get();

지연 즉시 로딩

때로는 부모 모델이 이미 검색된 후에 관계를 즉시 로드해야 할 수도 있습니다. 예를 들어, 관련 모델을 로드할지 여부를 동적으로 결정해야 하는 경우에 유용할 수 있습니다.

use App\Models\Book;
 
$books = Book::all();
 
if ($someCondition) {
$books->load('author', 'publisher');
}

즉시 로드 쿼리에 추가 쿼리 제약 조건을 설정해야 하는 경우 로드하려는 관계를 키로 하는 배열을 전달할 수 있습니다. 배열 값은 쿼리 인스턴스를 수신하는 클로저 인스턴스여야 합니다.

$author->load(['books' => function (Builder $query) {
$query->orderBy('published_date', 'asc');
}]);

아직 로드되지 않은 경우에만 관계를 로드하려면 loadMissing 메서드를 사용합니다.

$book->loadMissing('author');

중첩된 지연 즉시 로딩 및 morphTo

morphTo 관계와 해당 관계에서 반환될 수 있는 다양한 엔터티의 중첩된 관계를 즉시 로드하려면 loadMorph 메서드를 사용할 수 있습니다.

이 메서드는 morphTo 관계의 이름을 첫 번째 인수로 받고 모델/관계 쌍의 배열을 두 번째 인수로 받습니다. 이 메서드를 설명하기 위해 다음 모델을 고려해 보겠습니다.

<?php
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class ActivityFeed extends Model
{
/**
* 활동 피드 레코드의 부모를 가져옵니다.
*/
public function parentable(): MorphTo
{
return $this->morphTo();
}
}

이 예에서 Event, Photo, Post 모델이 ActivityFeed 모델을 만들 수 있다고 가정해 보겠습니다. 또한 Event 모델은 Calendar 모델에 속하고, Photo 모델은 Tag 모델과 연결되어 있으며, Post 모델은 Author 모델에 속한다고 가정해 보겠습니다.

이러한 모델 정의와 관계를 사용하여 ActivityFeed 모델 인스턴스를 검색하고 모든 parentable 모델과 해당 중첩된 관계를 즉시 로드할 수 있습니다.

$activities = ActivityFeed::with('parentable')
->get()
->loadMorph('parentable', [
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);

지연 로딩 방지

앞서 논의한 바와 같이 관계를 즉시 로드하면 응용 프로그램에 상당한 성능 이점을 제공할 수 있습니다. 따라서 원하는 경우 Laravel이 관계의 지연 로드를 항상 방지하도록 지시할 수 있습니다. 이를 수행하려면 기본 Eloquent 모델 클래스에서 제공하는 preventLazyLoading 메서드를 호출할 수 있습니다. 일반적으로 응용 프로그램의 AppServiceProvider 클래스의 boot 메서드 내에서 이 메서드를 호출해야 합니다.

preventLazyLoading 메서드는 지연 로드를 방지해야 하는지 여부를 나타내는 선택적 부울 인수를 허용합니다. 예를 들어, 프로덕션 환경에서 지연 로드된 관계가 실수로 있는 경우에도 프로덕션 환경이 정상적으로 계속 작동하도록 비프로덕션 환경에서만 지연 로드를 비활성화할 수 있습니다.

use Illuminate\Database\Eloquent\Model;
 
/**
* 모든 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}

레이지 로딩을 방지한 후, 애플리케이션이 Eloquent 관계를 레이지 로딩하려고 시도하면 Eloquent는 Illuminate\Database\LazyLoadingViolationException 예외를 발생시킵니다.

handleLazyLoadingViolationsUsing 메서드를 사용하여 레이지 로딩 위반의 동작을 사용자 정의할 수 있습니다. 예를 들어, 이 메서드를 사용하여 예외로 애플리케이션 실행을 중단시키는 대신 레이지 로딩 위반을 로깅만 하도록 지시할 수 있습니다:

Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
$class = $model::class;
 
info("모델 [{$class}]에서 [{$relation}]을(를) 레이지 로드하려고 시도했습니다.");
});

save 메서드

Eloquent는 관계에 새 모델을 추가하는 편리한 메서드를 제공합니다. 예를 들어, 게시물에 새로운 댓글을 추가해야 할 수 있습니다. Comment 모델에서 post_id 속성을 수동으로 설정하는 대신 관계의 save 메서드를 사용하여 댓글을 삽입할 수 있습니다.

use App\Models\Comment;
use App\Models\Post;
 
$comment = new Comment(['message' => '새로운 댓글입니다.']);
 
$post = Post::find(1);
 
$post->comments()->save($comment);

여기서 comments 관계에 동적 속성으로 접근하지 않았다는 점에 유의하십시오. 대신, 관계의 인스턴스를 얻기 위해 comments 메서드를 호출했습니다. save 메서드는 새로운 Comment 모델에 적절한 post_id 값을 자동으로 추가합니다.

여러 관련 모델을 저장해야 하는 경우 saveMany 메서드를 사용할 수 있습니다.

$post = Post::find(1);
 
$post->comments()->saveMany([
new Comment(['message' => '새로운 댓글입니다.']),
new Comment(['message' => '또 다른 새로운 댓글입니다.']),
]);

savesaveMany 메서드는 주어진 모델 인스턴스를 영구적으로 저장하지만, 이미 부모 모델에 로드된 메모리 내 관계에 새로 영구 저장된 모델을 추가하지 않습니다. save 또는 saveMany 메서드를 사용한 후 관계에 접근하려는 경우 refresh 메서드를 사용하여 모델과 해당 관계를 다시 로드할 수 있습니다.

$post->comments()->save($comment);
 
$post->refresh();
 
// 새로 저장된 댓글을 포함한 모든 댓글...
$post->comments;

모델 및 관계를 재귀적으로 저장

모델과 관련된 모든 관계를 save하려면 push 메서드를 사용할 수 있습니다. 이 예제에서 Post 모델은 댓글과 댓글 작성자와 함께 저장됩니다.

$post = Post::find(1);
 
$post->comments[0]->message = '메시지';
$post->comments[0]->author->name = '작성자 이름';
 
$post->push();

pushQuietly 메서드를 사용하여 이벤트를 발생시키지 않고 모델 및 관련 관계를 저장할 수 있습니다.

$post->pushQuietly();

create 메서드

savesaveMany 메서드 외에도 속성 배열을 받아 모델을 만들고 데이터베이스에 삽입하는 create 메서드를 사용할 수도 있습니다. savecreate의 차이점은 save는 전체 Eloquent 모델 인스턴스를 허용하는 반면 create는 일반 PHP array를 허용한다는 것입니다. 새로 생성된 모델은 create 메서드에 의해 반환됩니다.

use App\Models\Post;
 
$post = Post::find(1);
 
$comment = $post->comments()->create([
'message' => '새로운 댓글입니다.',
]);

여러 관련 모델을 생성하려면 createMany 메서드를 사용할 수 있습니다.

$post = Post::find(1);
 
$post->comments()->createMany([
['message' => '새로운 댓글입니다.'],
['message' => '또 다른 새로운 댓글입니다.'],
]);

createQuietlycreateManyQuietly 메서드를 사용하여 이벤트를 발생시키지 않고 모델을 만들 수 있습니다.

$user = User::find(1);
 
$user->posts()->createQuietly([
'title' => '게시물 제목.',
]);
 
$user->posts()->createManyQuietly([
['title' => '첫 번째 게시물.'],
['title' => '두 번째 게시물.'],
]);

findOrNew, firstOrNew, firstOrCreateupdateOrCreate 메서드를 사용하여 관계에 대한 모델을 생성하고 업데이트할 수도 있습니다.

lightbulb

create 메서드를 사용하기 전에 대량 할당 문서를 검토해야 합니다.

Belongs To 관계

자식 모델을 새 부모 모델에 할당하려면 associate 메서드를 사용할 수 있습니다. 이 예에서 User 모델은 Account 모델에 대한 belongsTo 관계를 정의합니다. 이 associate 메서드는 자식 모델에 외래 키를 설정합니다.

use App\Models\Account;
 
$account = Account::find(10);
 
$user->account()->associate($account);
 
$user->save();

자식 모델에서 부모 모델을 제거하려면 dissociate 메서드를 사용할 수 있습니다. 이 메서드는 관계의 외래 키를 null로 설정합니다.

$user->account()->dissociate();
 
$user->save();

다대다 관계

연결 / 분리

Eloquent는 또한 다대다 관계를 보다 편리하게 처리할 수 있는 방법을 제공합니다. 예를 들어, 사용자는 여러 역할을 가질 수 있고 역할은 여러 사용자를 가질 수 있다고 가정해 보겠습니다. 관계의 중간 테이블에 레코드를 삽입하여 사용자에게 역할을 연결하기 위해 attach 메서드를 사용할 수 있습니다.

use App\Models\User;
 
$user = User::find(1);
 
$user->roles()->attach($roleId);

모델에 관계를 연결할 때 중간 테이블에 삽입할 추가 데이터 배열을 전달할 수도 있습니다.

$user->roles()->attach($roleId, ['expires' => $expires]);

사용자로부터 역할을 제거해야 할 수 있습니다. 다대다 관계 레코드를 제거하려면 detach 메서드를 사용합니다. detach 메서드는 중간 테이블에서 적절한 레코드를 삭제합니다. 그러나 두 모델은 데이터베이스에 남아 있습니다.

// 사용자로부터 단일 역할 분리...
$user->roles()->detach($roleId);
 
// 사용자로부터 모든 역할 분리...
$user->roles()->detach();

편의를 위해 attachdetach는 ID 배열을 입력으로 허용합니다.

$user = User::find(1);
 
$user->roles()->detach([1, 2, 3]);
 
$user->roles()->attach([
1 => ['expires' => $expires],
2 => ['expires' => $expires],
]);

연결 동기화

sync 메서드를 사용하여 다대다 연결을 구성할 수도 있습니다. sync 메서드는 중간 테이블에 배치할 ID 배열을 허용합니다. 지정된 배열에 없는 ID는 중간 테이블에서 제거됩니다. 따라서 이 작업이 완료된 후 지정된 배열의 ID만 중간 테이블에 존재합니다.

$user->roles()->sync([1, 2, 3]);

ID와 함께 추가 중간 테이블 값을 전달할 수도 있습니다.

$user->roles()->sync([1 => ['expires' => true], 2, 3]);

동기화된 각 모델 ID와 동일한 중간 테이블 값을 삽입하려는 경우 syncWithPivotValues 메서드를 사용할 수 있습니다.

$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);

지정된 배열에서 누락된 기존 ID를 분리하지 않으려면 syncWithoutDetaching 메서드를 사용할 수 있습니다.

$user->roles()->syncWithoutDetaching([1, 2, 3]);

연결 토글

다대다 관계는 주어진 관련 모델 ID의 연결 상태를 "토글"하는 toggle 메서드도 제공합니다. 주어진 ID가 현재 연결되어 있으면 분리됩니다. 마찬가지로 현재 분리되어 있으면 연결됩니다.

$user->roles()->toggle([1, 2, 3]);

ID와 함께 추가 중간 테이블 값을 전달할 수도 있습니다.

$user->roles()->toggle([
1 => ['expires' => true],
2 => ['expires' => true],
]);

중간 테이블에서 레코드 업데이트

관계의 중간 테이블에서 기존 행을 업데이트해야 하는 경우 updateExistingPivot 메서드를 사용할 수 있습니다. 이 메서드는 업데이트할 중간 레코드 외래 키와 속성 배열을 허용합니다.

$user = User::find(1);
 
$user->roles()->updateExistingPivot($roleId, [
'active' => false,
]);

부모 타임스탬프 터치

모델이 belongsTo 또는 belongsToMany 관계를 다른 모델(예: Post에 속한 Comment)에 정의할 때 자식 모델이 업데이트될 때 부모의 타임스탬프를 업데이트하는 것이 유용한 경우가 있습니다.

예를 들어 Comment 모델이 업데이트되면 소유하는 Postupdated_at 타임스탬프를 자동으로 "터치"하여 현재 날짜 및 시간으로 설정할 수 있습니다. 이를 달성하려면 자식 모델이 업데이트될 때 updated_at 타임스탬프가 업데이트되어야 하는 관계의 이름을 포함하는 touches 속성을 자식 모델에 추가할 수 있습니다.

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Comment extends Model
{
/**
* 터치할 모든 관계.
*
* @var array
*/
protected $touches = ['post'];
 
/**
* 댓글이 속한 게시물을 가져옵니다.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}
exclamation

자식 모델이 Eloquent의 save 메서드를 사용하여 업데이트된 경우에만 부모 모델 타임스탬프가 업데이트됩니다.