This documentation is currently being translated. Some pages may appear in Korean.
Skip to main content
Version: master

Eloquent: 연관관계 (Eloquent: Relationships)

소개 (Introduction)

데이터베이스 테이블은 서로 관련되어 있는 경우가 많습니다. 예를 들어 블로그 게시물에는 여러 댓글이 있을 수 있고, 주문은 해당 주문을 생성한 사용자와 관련될 수 있습니다. Eloquent는 이러한 연관관계를 쉽게 관리하고 다룰 수 있게 해 주며, 여러 가지 일반적인 연관관계를 지원합니다:

연관관계 정의하기 (Defining Relationships)

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
{
/**
* Get the phone associated with the user.
*/
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 또는 모델의 기본 키가 아닌 다른 기본 키 값을 사용하도록 하려면 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
{
/**
* Get the user that owns the phone.
*/
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 메서드의 두 번째 인수로 커스텀 키 이름을 전달할 수 있습니다:

/**
* Get the user that owns the phone.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'foreign_key');
}

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

/**
* Get the user that owns the phone.
*/
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
{
/**
* Get the comments for the blog post.
*/
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}

Eloquent는 Comment 모델에 사용할 적절한 외래 키 컬럼을 자동으로 결정한다는 점을 기억하십시오. 관례적으로 Eloquent는 부모 모델 이름을 "snake case"로 변환한 뒤 _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;
}
}

위 예제에서는 모든 Post 모델에 대해 댓글을 즉시 로딩했음에도, Eloquent가 각 자식 Comment 모델에 부모 Post를 자동으로 하이드레이션하지 않기 때문에 "N + 1" 쿼리 문제가 발생합니다.

Eloquent가 부모 모델을 자식 모델에 자동으로 하이드레이션하도록 하려면, hasMany 연관관계를 정의할 때 chaperone 메서드를 호출할 수 있습니다:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Post extends Model
{
/**
* Get the comments for the blog post.
*/
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
{
/**
* Get the post that owns the comment.
*/
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 메서드의 두 번째 인수로 커스텀 외래 키 이름을 전달할 수 있습니다:

/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key');
}

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

/**
* Get the post that owns the comment.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}

기본 모델

belongsTo, hasOne, hasOneThrough, morphOne 연관관계에서는 해당 연관관계가 null일 때 반환할 기본 모델을 정의할 수 있습니다. 이 패턴은 흔히 Null Object pattern이라고 하며, 코드에서 조건문 검사를 줄이는 데 도움이 됩니다. 다음 예제에서 Post 모델에 연결된 사용자가 없다면 user 연관관계는 빈 App\Models\User 모델을 반환합니다:

/**
* Get the author of the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault();
}

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

/**
* Get the author of the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault([
'name' => 'Guest Author',
]);
}

/**
* Get the author of the post.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withDefault(function (User $user, Post $post) {
$user->name = 'Guest Author';
});
}

Belongs To 연관관계 쿼리하기

"belongs to" 연관관계의 자식 모델을 쿼리할 때는, 해당 Eloquent 모델을 조회하기 위해 where 절을 직접 작성할 수 있습니다:

use App\Models\Post;

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

하지만 주어진 모델에 적절한 연관관계와 외래 키를 자동으로 결정해 주는 whereBelongsTo 메서드를 사용하면 더 편리할 수 있습니다:

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

whereBelongsTo 메서드에는 컬렉션 인스턴스를 제공할 수도 있습니다. 이 경우 Laravel은 컬렉션 안의 부모 모델 중 하나에 속하는 모델을 조회합니다:

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

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

기본적으로 Laravel은 주어진 모델의 클래스 이름을 기준으로 해당 모델과 연결된 연관관계를 결정합니다. 그러나 whereBelongsTo 메서드의 두 번째 인수로 연관관계 이름을 직접 지정할 수 있습니다:

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

다수 중 하나를 가지는 관계

때로는 하나의 모델이 여러 관련 모델을 가질 수 있지만, 그 연관관계에서 "latest" 또는 "oldest" 관련 모델을 쉽게 가져오고 싶을 수 있습니다. 예를 들어, User 모델은 여러 Order 모델과 관련될 수 있지만, 사용자가 가장 최근에 주문한 내역과 편리하게 상호작용하는 방법을 정의하고 싶을 수 있습니다. 이는 hasOne 연관관계 타입과 ofMany 메서드를 함께 사용하여 구현할 수 있습니다.

/**
* Get the user's most recent order.
*/
public function latestOrder(): HasOne
{
return $this->hasOne(Order::class)->latestOfMany();
}

마찬가지로, 연관관계에서 "oldest", 즉 첫 번째 관련 모델을 가져오는 메서드를 정의할 수도 있습니다.

/**
* Get the user's oldest order.
*/
public function oldestOrder(): HasOne
{
return $this->hasOne(Order::class)->oldestOfMany();
}

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

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

/**
* Get the user's largest order.
*/
public function largestOrder(): HasOne
{
return $this->hasOne(Order::class)->ofMany('price', 'max');
}

PostgreSQL은 UUID 컬럼에 대해 MAX 함수를 실행하는 것을 지원하지 않으므로, 현재 PostgreSQL UUID 컬럼과 함께 다수 중 하나(one-of-many) 연관관계를 사용할 수 없습니다.

"다수" 연관관계를 Has One 연관관계로 변환하기

latestOfMany, oldestOfMany, 또는 ofMany 메서드를 사용하여 단일 모델을 가져올 때, 같은 모델에 대해 이미 "has many" 연관관계를 정의해 둔 경우가 많습니다. 편의를 위해 Laravel은 해당 연관관계에서 one 메서드를 호출하여 이 연관관계를 "has one" 연관관계로 쉽게 변환할 수 있게 해 줍니다.

/**
* Get the user's orders.
*/
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}

/**
* Get the user's largest order.
*/
public function largestOrder(): HasOne
{
return $this->orders()->one()->ofMany('price', 'max');
}

또한 one 메서드를 사용하여 HasManyThrough 연관관계를 HasOneThrough 연관관계로 변환할 수도 있습니다.

public function latestDeployment(): HasOneThrough
{
return $this->deployments()->one()->latestOfMany();
}

고급 다수 중 하나를 가지는 관계

더 고급 형태의 "has one of many" 연관관계를 구성할 수도 있습니다. 예를 들어 Product 모델은 여러 관련 Price 모델을 가질 수 있으며, 새로운 가격이 게시된 이후에도 기존 가격 데이터가 시스템에 보관될 수 있습니다. 또한 상품의 새로운 가격 데이터는 published_at 컬럼을 통해 미래 시점에 적용되도록 미리 게시될 수도 있습니다.

정리하면, 미래가 아닌 게시 날짜를 가진 가격 중 가장 최신에 게시된 가격을 가져와야 합니다. 또한 두 가격의 게시 날짜가 같다면 ID가 더 큰 가격을 우선해야 합니다. 이를 구현하려면 최신 가격을 결정하는 정렬 가능한 컬럼을 포함한 배열을 ofMany 메서드에 전달해야 합니다. 또한 ofMany 메서드의 두 번째 인수로 클로저를 제공합니다. 이 클로저는 연관관계 쿼리에 추가적인 게시 날짜 제약 조건을 더하는 역할을 합니다.

/**
* Get the current pricing for the product.
*/
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" 연관관계는 다른 모델과의 일대일 연관관계를 정의합니다. 다만 이 연관관계는 선언하는 모델이 세 번째 모델을 거쳐 다른 모델의 한 인스턴스와 연결될 수 있음을 나타냅니다.

예를 들어 차량 수리점 애플리케이션에서 각 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" 연관관계를 정의할 수 있습니다.

// String based syntax...
return $this->through('cars')->has('owner');

// Dynamic syntax...
return $this->throughCars()->hasOwner();

키 규칙

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

class Mechanic extends Model
{
/**
* Get the car's owner.
*/
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(
Owner::class,
Car::class,
'mechanic_id', // Foreign key on the cars table...
'car_id', // Foreign key on the owners table...
'id', // Local key on the mechanics table...
'id' // Local key on the cars table...
);
}
}

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

// String based syntax...
return $this->through('cars')->has('owner');

// Dynamic syntax...
return $this->throughCars()->hasOwner();

하나를 거쳐 다수를 가지는 관계

"has-many-through" 연관관계는 중간 연관관계를 통해 멀리 떨어진 관계에 편리하게 접근하는 방법을 제공합니다. 예를 들어 Laravel Cloud와 같은 배포 플랫폼을 만들고 있다고 가정해 보겠습니다. Application 모델은 중간 Environment 모델을 통해 여러 Deployment 모델에 접근할 수 있습니다. 이 예제를 사용하면 주어진 애플리케이션의 모든 배포를 쉽게 가져올 수 있습니다. 이 연관관계를 정의하는 데 필요한 테이블을 살펴보겠습니다.

applications
id - integer
name - string

environments
id - integer
application_id - integer
name - string

deployments
id - integer
environment_id - integer
commit_hash - string

이제 연관관계의 테이블 구조를 살펴보았으니, Application 모델에 연관관계를 정의해 보겠습니다.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;

class Application extends Model
{
/**
* Get all of the deployments for the application.
*/
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(Deployment::class, Environment::class);
}
}

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

또는 이 연관관계에 포함된 모든 모델에 관련 연관관계가 이미 정의되어 있다면, through 메서드를 호출하고 해당 연관관계 이름을 제공하여 "has-many-through" 연관관계를 유창하게 정의할 수 있습니다. 예를 들어 Application 모델에 environments 연관관계가 있고 Environment 모델에 deployments 연관관계가 있다면, 다음과 같이 애플리케이션과 배포를 연결하는 "has-many-through" 연관관계를 정의할 수 있습니다.

// String based syntax...
return $this->through('environments')->has('deployments');

// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();

Deployment 모델의 테이블에는 application_id 컬럼이 없지만, hasManyThrough 연관관계는 $application->deployments를 통해 애플리케이션의 배포에 접근할 수 있게 해 줍니다. 이러한 모델을 가져오기 위해 Eloquent는 중간 Environment 모델 테이블의 application_id 컬럼을 확인합니다. 관련 환경 ID를 찾은 뒤, 그 ID를 사용해 Deployment 모델의 테이블을 조회합니다.

키 규칙

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

class Application extends Model
{
public function deployments(): HasManyThrough
{
return $this->hasManyThrough(
Deployment::class,
Environment::class,
'application_id', // Foreign key on the environments table...
'environment_id', // Foreign key on the deployments table...
'id', // Local key on the applications table...
'id' // Local key on the environments table...
);
}
}

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

// String based syntax...
return $this->through('environments')->has('deployments');

// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();

범위가 지정된 연관관계

연관관계에 제약 조건을 추가하는 메서드를 모델에 더하는 일은 흔합니다. 예를 들어 User 모델에 featuredPosts 메서드를 추가하여 더 넓은 posts 연관관계에 추가 where 제약 조건을 적용할 수 있습니다.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Model
{
/**
* Get the user's posts.
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class)->latest();
}

/**
* Get the user's featured posts.
*/
public function featuredPosts(): HasMany
{
return $this->posts()->where('featured', true);
}
}

그러나 featuredPosts 메서드를 통해 모델을 생성하려고 하면, 해당 모델의 featured 속성은 true로 설정되지 않습니다. 연관관계 메서드를 통해 모델을 생성하면서, 그 연관관계를 통해 생성되는 모든 모델에 추가해야 할 속성도 함께 지정하고 싶다면, 연관관계 쿼리를 만들 때 withAttributes 메서드를 사용할 수 있습니다.

/**
* Get the user's featured posts.
*/
public function featuredPosts(): HasMany
{
return $this->posts()->withAttributes(['featured' => true]);
}

withAttributes 메서드는 주어진 속성을 사용하여 쿼리에 where 조건을 추가하며, 해당 연관관계 메서드를 통해 생성되는 모든 모델에도 주어진 속성을 추가합니다.

$post = $user->featuredPosts()->create(['title' => 'Featured Post']);

$post->featured; // true

withAttributes 메서드가 쿼리에 where 조건을 추가하지 않도록 하려면, asConditions 인수를 false로 설정하면 됩니다.

return $this->posts()->withAttributes(['featured' => true], asConditions: false);

다대다 연관관계 (Many to Many Relationships)

다대다 연관관계는 hasOnehasMany 연관관계보다 조금 더 복잡합니다. 다대다 연관관계의 예로는 한 사용자가 여러 역할을 가지고, 그 역할들이 애플리케이션의 다른 사용자들과도 공유되는 경우가 있습니다. 예를 들어 한 사용자는 "Author"와 "Editor" 역할을 부여받을 수 있습니다. 하지만 이러한 역할은 다른 사용자에게도 부여될 수 있습니다. 따라서 사용자는 여러 역할을 가지고, 하나의 역할도 여러 사용자를 가집니다.

테이블 구조

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

역할은 여러 사용자에게 속할 수 있으므로, roles 테이블에 단순히 user_id 컬럼을 둘 수는 없습니다. 그렇게 하면 하나의 역할이 한 명의 사용자에게만 속할 수 있다는 의미가 됩니다. 여러 사용자에게 역할을 부여할 수 있도록 지원하려면 role_user 테이블이 필요합니다. 이 연관관계의 테이블 구조는 다음과 같이 요약할 수 있습니다.

users
id - integer
name - string

roles
id - integer
name - string

role_user
user_id - integer
role_id - integer

모델 구조

다대다 연관관계는 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
{
/**
* The roles that belong to the user.
*/
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
{
/**
* The users that belong to the role.
*/
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();

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, wherePivotNull, wherePivotNotNull 메서드를 사용하여 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');

wherePivot은 쿼리에 where 절 제약 조건을 추가하지만, 정의된 연관관계를 통해 새 모델을 만들 때 지정된 값을 추가하지는 않습니다. 특정 pivot 값을 사용해 연관관계를 조회하면서 생성도 해야 한다면 withPivotValue 메서드를 사용할 수 있습니다.

return $this->belongsToMany(Role::class)
->withPivotValue('approved', 1);

중간 테이블 컬럼으로 쿼리 정렬

orderByPivotorderByPivotDesc 메서드를 사용하여 belongsToMany 연관관계 쿼리가 반환하는 결과를 정렬할 수 있습니다. 다음 예제에서는 사용자의 최신 배지를 모두 조회합니다.

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

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

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

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

<?php

namespace App\Models;

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

class Role extends Model
{
/**
* The users that belong to the role.
*/
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
{
// ...
}

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

사용자 지정 Pivot 모델과 증가 ID

사용자 지정 pivot 모델을 사용하는 다대다 연관관계를 정의했고, 해당 pivot 모델에 자동 증가 기본 키가 있다면, 사용자 지정 pivot 모델 클래스가 incrementingtrue로 설정된 Table 속성을 사용하도록 해야 합니다.

use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Relations\Pivot;

#[Table(incrementing: true)]
class RoleUser extends Pivot
{
// ...
}

다형성 연관관계 (Polymorphic Relationships)

다형성 연관관계를 사용하면 자식 모델이 하나의 연결을 통해 여러 종류의 모델에 속할 수 있습니다. 예를 들어 사용자가 블로그 게시물과 비디오를 공유할 수 있는 애플리케이션을 만들고 있다고 상상해 보십시오. 이런 애플리케이션에서는 Comment 모델이 Post 모델과 Video 모델 모두에 속할 수 있습니다.

일대일 (다형성)

테이블 구조

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

posts
id - integer
name - string

users
id - integer
name - string

images
id - integer
url - string
imageable_id - integer
imageable_type - string

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
{
/**
* Get the parent imageable model (user or post).
*/
public function imageable(): MorphTo
{
return $this->morphTo();
}
}

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;

class Post extends Model
{
/**
* Get the post's image.
*/
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
{
/**
* Get the user's image.
*/
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__ 상수를 사용할 수 있습니다.

/**
* Get the model that the image belongs to.
*/
public function imageable(): MorphTo
{
return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}

일대다 (다형성)

테이블 구조

일대다 다형성 연관관계는 일반적인 일대다 연관관계와 비슷합니다. 하지만 자식 모델은 하나의 연결을 통해 여러 종류의 모델에 속할 수 있습니다. 예를 들어 애플리케이션의 사용자가 게시물과 비디오에 "댓글"을 달 수 있다고 상상해 보십시오. 다형성 연관관계를 사용하면 하나의 comments 테이블에 게시물과 비디오의 댓글을 모두 담을 수 있습니다. 먼저 이 연관관계를 만들기 위해 필요한 테이블 구조를 살펴보겠습니다.

posts
id - integer
title - string
body - text

videos
id - integer
title - string
url - string

comments
id - integer
body - text
commentable_id - integer
commentable_type - string

모델 구조

다음으로 이 연관관계를 만들기 위해 필요한 모델 정의를 살펴보겠습니다.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class Comment extends Model
{
/**
* Get the parent commentable model (post or video).
*/
public function commentable(): MorphTo
{
return $this->morphTo();
}
}

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;

class Post extends Model
{
/**
* Get all of the post's comments.
*/
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
{
/**
* Get all of the video's comments.
*/
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;
}
}

위 예시에서는 각 Post 모델에 대해 댓글을 즉시 로딩했지만, Eloquent가 각 자식 Comment 모델에 부모 Post를 자동으로 하이드레이션하지 않기 때문에 "N + 1" 쿼리 문제가 발생합니다.

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

class Post extends Model
{
/**
* Get all of the post's comments.
*/
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();

여러 개 중 하나

때로는 한 모델에 여러 관련 모델이 있지만, 그 연관관계에서 "latest" 또는 "oldest" 관련 모델을 쉽게 조회하고 싶을 수 있습니다. 예를 들어 User 모델은 여러 Image 모델과 연결될 수 있지만, 사용자가 업로드한 가장 최근 이미지를 편리하게 다루는 방법을 정의하고 싶을 수 있습니다. morphOne 연관관계 타입과 ofMany 메서드를 함께 사용하면 이를 구현할 수 있습니다.

/**
* Get the user's most recent image.
*/
public function latestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}

마찬가지로 연관관계에서 "oldest", 즉 첫 번째 관련 모델을 조회하는 메서드를 정의할 수도 있습니다.

/**
* Get the user's oldest image.
*/
public function oldestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}

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

예를 들어 ofMany 메서드를 사용하면 사용자의 가장 "liked"가 많은 이미지를 조회할 수 있습니다. ofMany 메서드는 첫 번째 인수로 정렬 가능한 컬럼을 받고, 관련 모델을 쿼리할 때 적용할 집계 함수(min 또는 max)를 두 번째 인수로 받습니다.

/**
* Get the user's most popular image.
*/
public function bestImage(): MorphOne
{
return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}

더 고급 "여러 개 중 하나" 연관관계를 구성할 수도 있습니다. 자세한 내용은 has one of many 문서를 참고하십시오.

다대다

테이블 구조

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

posts
id - integer
name - string

videos
id - integer
name - string

tags
id - integer
name - string

taggables
tag_id - integer
taggable_id - integer
taggable_type - string

다형성 다대다 연관관계를 자세히 살펴보기 전에, 일반적인 다대다 연관관계에 대한 문서를 읽어두면 도움이 될 수 있습니다.

모델 구조

다음으로 모델에 연관관계를 정의할 준비가 되었습니다. 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
{
/**
* Get all of the tags for the post.
*/
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
{
/**
* Get all of the posts that are assigned this tag.
*/
public function posts(): MorphToMany
{
return $this->morphedByMany(Post::class, 'taggable');
}

/**
* Get all of the videos that are assigned this tag.
*/
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은 관련 모델의 "type"을 저장할 때 정규화된 클래스 이름을 사용합니다. 예를 들어 위의 일대다 연관관계 예시에서 Comment 모델이 Post 또는 Video 모델에 속할 수 있다면, 기본 commentable_type은 각각 App\Models\Post 또는 App\Models\Video가 됩니다. 하지만 이러한 값을 애플리케이션 내부 구조에서 분리하고 싶을 수 있습니다.

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

use Illuminate\Database\Eloquent\Relations\Relation;

Relation::enforceMorphMap([
'post' => 'App\Models\Post',
'video' => 'App\Models\Video',
]);

원한다면 App\Providers\AppServiceProvider 클래스의 boot 메서드에서 enforceMorphMap 메서드를 호출하거나 별도의 서비스 프로바이더를 만들 수 있습니다.

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

use Illuminate\Database\Eloquent\Relations\Relation;

$alias = $post->getMorphClass();

$class = Relation::getMorphedModel($alias);

기존 애플리케이션에 "morph map"을 추가할 때는, 데이터베이스에서 아직 정규화된 클래스를 포함하고 있는 모든 morph 가능 *_type 컬럼 값을 해당 "map" 이름으로 변환해야 합니다.

동적 연관관계

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

resolveRelationUsing 메서드는 첫 번째 인수로 원하는 연관관계 이름을 받습니다. 두 번째 인수로 전달되는 값은 모델 인스턴스를 받고 유효한 Eloquent 연관관계 정의를 반환하는 클로저여야 합니다. 일반적으로 동적 연관관계는 서비스 프로바이더의 boot 메서드 안에서 설정해야 합니다.

use App\Models\Order;
use App\Models\Customer;

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

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

연관관계 쿼리하기 (Querying Relations)

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

예를 들어, User 모델이 여러 관련 Post 모델을 가지는 블로그 애플리케이션을 생각해 보겠습니다.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Model
{
/**
* Get all of the posts for the user.
*/
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)

연관관계 메서드와 동적 속성

Eloquent 연관관계 쿼리에 추가 제약 조건을 더할 필요가 없다면, 연관관계를 속성처럼 접근할 수 있습니다. 예를 들어 앞서 사용한 UserPost 예시 모델을 이어서 보면, 다음과 같이 사용자의 모든 게시물에 접근할 수 있습니다.

use App\Models\User;

$user = User::find(1);

foreach ($user->posts as $post) {
// ...
}

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

연관관계 존재 여부 쿼리하기

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

use App\Models\Post;

// Retrieve all posts that have at least one comment...
$posts = Post::has('comments')->get();

연산자와 개수 값을 지정하여 쿼리를 더 세밀하게 조정할 수도 있습니다.

// Retrieve all posts that have three or more comments...
$posts = Post::has('comments', '>=', 3)->get();

중첩된 has 구문은 "dot" 표기법으로 구성할 수 있습니다. 예를 들어, 이미지가 하나 이상 있는 댓글을 하나 이상 가진 모든 게시물을 조회할 수 있습니다.

// Retrieve posts that have at least one comment with images...
$posts = Post::has('comments.images')->get();

더 강력한 기능이 필요하다면 whereHasorWhereHas 메서드를 사용하여 has 쿼리에 댓글 내용을 검사하는 것과 같은 추가 쿼리 제약 조건을 정의할 수 있습니다.

use Illuminate\Database\Eloquent\Builder;

// Retrieve posts with at least one comment containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();

// Retrieve posts with at least ten comments containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
}, '>=', 10)->get();

Eloquent는 현재 데이터베이스를 가로지르는 연관관계 존재 여부 쿼리를 지원하지 않습니다. 연관관계는 반드시 같은 데이터베이스 안에 존재해야 합니다.

다대다 연관관계 존재 쿼리

whereAttachedTo 메서드는 특정 모델 또는 모델 컬렉션에 다대다로 연결된 모델을 조회하는 데 사용할 수 있습니다.

$users = User::whereAttachedTo($role)->get();

whereAttachedTo 메서드에는 컬렉션 인스턴스를 전달할 수도 있습니다. 이 경우 Laravel은 컬렉션 안의 모델 중 하나라도 연결된 모델을 조회합니다.

$tags = Tag::whereLike('name', '%laravel%')->get();

$posts = Post::whereAttachedTo($tags)->get();

인라인 연관관계 존재 쿼리

연관관계 쿼리에 하나의 단순한 where 조건만 붙여 연관관계의 존재 여부를 조회하고 싶다면, whereRelation, orWhereRelation, whereMorphRelation, orWhereMorphRelation 메서드를 사용하는 것이 더 편리할 수 있습니다. 예를 들어, 승인되지 않은 댓글이 있는 모든 게시물을 조회할 수 있습니다.

use App\Models\Post;

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

물론 쿼리 빌더의 where 메서드를 호출할 때처럼 연산자를 지정할 수도 있습니다.

$posts = Post::whereRelation(
'comments', 'created_at', '>=', now()->minus(hours: 1)
)->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();

"dot" 표기법을 사용하여 중첩된 연관관계에 대해 쿼리를 실행할 수 있습니다. 예를 들어, 다음 쿼리는 댓글이 없는 모든 게시물과, 댓글은 있지만 그 댓글 중 금지된 사용자에게 작성된 댓글이 하나도 없는 게시물을 조회합니다.

use Illuminate\Database\Eloquent\Builder;

$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {
$query->where('banned', 1);
})->get();

Morph To 연관관계 쿼리

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

use App\Models\Comment;
use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Eloquent\Builder;

// Retrieve comments associated to posts or videos with a title like code%...
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Video::class],
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();

// Retrieve comments associated to posts with a title not like 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" 연관관계 부모의 자식들을 조회하고 싶을 수 있습니다. 이 작업은 whereMorphedTowhereNotMorphedTo 메서드를 사용하여 수행할 수 있으며, 이 메서드들은 주어진 모델에 맞는 적절한 morph 타입 매핑을 자동으로 결정합니다. 이 메서드들은 첫 번째 인수로 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 메서드는 결과 모델에 {relation}_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();

기타 집계 함수

withCount 메서드 외에도 Eloquent는 withMin, withMax, withAvg, withSum, withExists 메서드를 제공합니다. 이 메서드들은 결과 모델에 {relation}_{function}_{column} 속성을 추가합니다.

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 모델은 parentable이라는 "morph to" 연관관계를 정의하며, 이를 통해 주어진 ActivityFeed 인스턴스의 부모 Photo 또는 Post 모델을 조회할 수 있다고 가정합니다. 또한 Photo 모델은 Tag 모델을 "여러 개 가지고 있고", Post 모델은 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 모델 집합을 조회했으며, 이제 activity feed와 연결된 다양한 parentable 모델의 중첩된 연관관계 개수를 로드하고 싶다고 가정해 보겠습니다. 이를 위해 loadMorphCount 메서드를 사용할 수 있습니다.

$activities = ActivityFeed::with('parentable')->get();

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

즉시 로딩 (Eager Loading)

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Book extends Model
{
/**
* Get the author that wrote the book.
*/
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;
}

이 작업에서는 두 개의 쿼리만 실행됩니다. 모든 책을 조회하는 쿼리 하나와, 모든 책에 대한 모든 저자를 조회하는 쿼리 하나입니다.

select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

여러 연관관계 즉시 로딩

때로는 여러 개의 서로 다른 연관관계를 즉시 로딩해야 할 수 있습니다. 이를 위해 with 메서드에 연관관계 배열을 전달하면 됩니다.

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

중첩 즉시 로딩

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

$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
{
/**
* Get the parent of the activity feed record.
*/
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();

이 기능을 사용할 때는 가져오려는 컬럼 목록에 항상 id 컬럼과 관련된 외래 키 컬럼을 포함해야 합니다.

기본 즉시 로딩

모델을 조회할 때 특정 연관관계를 항상 로드하고 싶을 때가 있습니다. 이를 위해 모델에 $with 속성을 정의할 수 있습니다.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Book extends Model
{
/**
* The relationships that should always be loaded.
*
* @var array
*/
protected $with = ['author'];

/**
* Get the author that wrote the book.
*/
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}

/**
* Get the genre of the book.
*/
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;

$users = User::with(['posts' => function ($query) {
$query->where('title', 'like', '%code%');
}])->get();

이 예제에서 Eloquent는 게시물의 title 컬럼에 code라는 단어가 포함된 게시물만 즉시 로드합니다. 즉시 로딩 작업을 더 세부적으로 조정하려면 다른 쿼리 빌더 메서드를 호출할 수 있습니다.

$users = User::with(['posts' => function ($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 ($condition) {
$books->load('author', 'publisher');
}

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

$author->load(['books' => function ($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
{
/**
* Get the parent of the activity feed record.
*/
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은 접근하는 연관관계를 자동으로 즉시 로드할 수 있습니다. 자동 즉시 로딩을 활성화하려면 애플리케이션의 AppServiceProvider에 있는 boot 메서드 안에서 Model::automaticallyEagerLoadRelationships 메서드를 호출해야 합니다.

use Illuminate\Database\Eloquent\Model;

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::automaticallyEagerLoadRelationships();
}

이 기능이 활성화되면 Laravel은 아직 로드되지 않은 연관관계에 접근할 때 해당 연관관계를 자동으로 로드하려고 시도합니다. 예를 들어 다음 상황을 살펴보겠습니다.

use App\Models\User;

$users = User::all();

foreach ($users as $user) {
foreach ($user->posts as $post) {
foreach ($post->comments as $comment) {
echo $comment->content;
}
}
}

일반적으로 위 코드는 각 사용자의 게시물을 가져오기 위해 사용자마다 쿼리를 실행하고, 각 게시물의 댓글을 가져오기 위해 게시물마다 쿼리를 실행합니다. 하지만 automaticallyEagerLoadRelationships 기능이 활성화되어 있으면, 조회된 사용자 중 어떤 사용자에서든 게시물에 접근하는 순간 Laravel은 사용자 컬렉션의 모든 사용자에 대해 게시물을 자동으로 지연 즉시 로드합니다. 마찬가지로 조회된 게시물 중 어떤 게시물에서든 댓글에 접근하면, 원래 조회된 모든 게시물에 대해 모든 댓글이 지연 즉시 로드됩니다.

자동 즉시 로딩을 전역으로 활성화하고 싶지 않다면, 컬렉션에서 withRelationshipAutoloading 메서드를 호출하여 단일 Eloquent 컬렉션 인스턴스에 대해서만 이 기능을 활성화할 수도 있습니다.

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

return $users->withRelationshipAutoloading();

지연 로딩 방지

앞서 설명한 것처럼, 연관관계의 즉시 로딩은 애플리케이션에 상당한 성능상의 이점을 제공할 수 있습니다. 따라서 원한다면 Laravel이 항상 연관관계의 지연 로딩을 방지하도록 지시할 수 있습니다. 이를 위해 기본 Eloquent 모델 클래스에서 제공하는 preventLazyLoading 메서드를 호출할 수 있습니다. 일반적으로 이 메서드는 애플리케이션의 AppServiceProvider 클래스에 있는 boot 메서드 안에서 호출해야 합니다.

preventLazyLoading 메서드는 지연 로딩을 방지할지 여부를 나타내는 선택적 boolean 인수를 받습니다. 예를 들어, 프로덕션 코드에 지연 로딩되는 연관관계가 실수로 포함되어 있더라도 프로덕션 환경은 정상적으로 동작하도록 유지하면서, 비프로덕션 환경에서만 지연 로딩을 비활성화하고 싶을 수 있습니다.

use Illuminate\Database\Eloquent\Model;

/**
* Bootstrap any application services.
*/
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("Attempted to lazy load [{$relation}] on model [{$class}].");
});

save 메서드

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

use App\Models\Comment;
use App\Models\Post;

$comment = new Comment(['message' => 'A new comment.']);

$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' => 'A new comment.']),
new Comment(['message' => 'Another new comment.']),
]);

savesaveMany 메서드는 전달된 모델 인스턴스를 영구 저장하지만, 새로 저장된 모델을 부모 모델에 이미 로드되어 있는 메모리상의 연관관계에는 추가하지 않습니다. save 또는 saveMany 메서드를 사용한 뒤 연관관계에 접근할 계획이라면, refresh 메서드를 사용하여 모델과 그 연관관계를 다시 로드하는 것이 좋습니다.

$post->comments()->save($comment);

$post->refresh();

// All comments, including the newly saved comment...
$post->comments;

모델과 연관관계를 재귀적으로 저장하기

모델과 그에 연결된 모든 연관관계를 save하고 싶다면 push 메서드를 사용할 수 있습니다. 이 예제에서는 Post 모델과 함께 댓글, 그리고 댓글의 작성자가 저장됩니다.

$post = Post::find(1);

$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = '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' => 'A new comment.',
]);

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

$post = Post::find(1);

$post->comments()->createMany([
['message' => 'A new comment.'],
['message' => 'Another new comment.'],
]);

createQuietlycreateManyQuietly 메서드는 이벤트를 디스패치하지 않고 모델을 생성하는 데 사용할 수 있습니다.

$user = User::find(1);

$user->posts()->createQuietly([
'title' => 'Post title.',
]);

$user->posts()->createManyQuietly([
['title' => 'First post.'],
['title' => 'Second post.'],
]);

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

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 메서드는 중간 테이블에서 해당 레코드를 삭제하지만, 두 모델은 모두 데이터베이스에 그대로 남아 있습니다.

// Detach a single role from the user...
$user->roles()->detach($roleId);

// Detach all roles from the user...
$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,
]);

부모 타임스탬프 갱신하기 (Touching Parent Timestamps)

모델이 다른 모델에 대한 belongsTo 또는 belongsToMany 연관관계를 정의할 때가 있습니다. 예를 들어 CommentPost에 속하는 경우입니다. 이런 경우 자식 모델이 업데이트될 때 부모 모델의 타임스탬프도 함께 업데이트하면 유용할 때가 있습니다.

예를 들어 Comment 모델이 업데이트될 때, 소유자인 Postupdated_at 타임스탬프를 자동으로 “touch”하여 현재 날짜와 시간으로 설정하고 싶을 수 있습니다. 이를 위해 자식 모델에 Touches 속성을 사용할 수 있습니다. 이 속성에는 자식 모델이 업데이트될 때 updated_at 타임스탬프가 함께 업데이트되어야 하는 연관관계 이름을 지정합니다.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Touches;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

#[Touches(['post'])]
class Comment extends Model
{
/**
* Get the post that the comment belongs to.
*/
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}

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