[Laravel]with() | has() | whereHas() 뭐가 다를까

ORM도 익숙하지 않지만, Eloquent ORM은 처음 접해보면서,

쿼리빌더를 사용하는것은 ORM스럽지 못하다 느껴 최대한 ORM으로 풀고 싶었습니다

검색으로 알아보던중 Eloquent:Relations에 대해 알게되었습니다.

라라벨 공식 문서 : 관계의 존재 여부 쿼리 질의하기

예를들어 User가 여러개의 Post 를 가지고 있다면,

User 클래스에 $this->hasMany('App\Post');를 설정해 두었을 것이라는 가정하에 설명을 진행하도록 하겠습니다.

with()

  • user
    • id
    • name
  • post
    • id
    • user_id
    • title
      이와 같은 테이블 구조에서 유저가 작성한 글의 제목을 조회하려면
      1
      2
      3
      4
      $users = selectAll("select * from `user`);
      foreach ($users as $user) {
      $user['post_title'] = selectOne("select `title` from `post` where `post`.`user_id` = {$user['id']}");
      }
      회원을 조회하고, 회원들을 순회하며 게시물을 조회하는 코드입니다.

      이 경우 회원이 1000명일 경우 쿼리는 1001번 실행하게 됩니다.

      이것을 N+1쿼리 문제 라고 합니다.

사람들에 따라 직관적이여서 선호하기도 하고,

join으로 실행시 쿼리가 오래 걸리면 이와 같이 분리하여 사용하기도 합니다.
또는 join으로 해결할 수 없는 상황에서도 사용합니다.

이 문제를 해결 하기 위해

1
$users = selectAll("select `user`.*,`post`.`title` as post_title from `user` left join `post` on `post`.`user_id` = `user`.`id`"); 

join으로 해결할 수 없는 경우를 제외 하고는 위와 같이 join을 사용해서 조회 할 수 있습니다.

또한 이 문제는 ORM에서 주로 발생 합니다.

ORM을 사용하면 user와 post의 관계를 설정하고, 아래와 같이 사용할 수 있습니다.

1
2
3
foreach (User::all() as $user) {
echo $user->post->title;
}

해당 코드는

1
2
3
4
5
6
select * from `user`; # 1, 2, 3, 4, 5, 6....
select * from `post` where id = 1;
select * from `post` where id = 2;
select * from `post` where id = 3;
select * from `post` where id = 4;
...

user를 조회하고, user의 수 만큼 post를 조회합니다.

위의 N+1 문제와 같은 문제입니다.

이 문제를 해결 하기 위한 방안으로는 즉시 로딩(Eager Loading)이 있습니다.

1
2
3
foreach (User::with('posts')->get() as $user){
echo $user->post->title;
}

이와같이 with()를 사용하면 미리 선언한 관계를 사용하여 같이 가져올수있습니다.

언뜻 보기엔 별 다를바 없어 보이는 코드이지만 with 메소드를 사용하면, user와 연관된 post를 미리 로드합니다.

실행되는 쿼리는

1
2
select * from `user`; # 1, 2, 3, 4, 5, 6....
select * from `post` where `id` in (1, 2, 3, 4, 5, 6, ...);

위와 같이 쿼리 2개만 실행이 되어 쿼리 실행을 최소화 할 수 있습니다.

with() 메소드의 2번째 파라미터를 사용해 제한 할 수도 있습니다.

1
2
3
4
5
6
7
foreach(User::with(['posts' => function ($query) {
$query->where('title', 'like', '치킨%')
}
])->get() as $user) {
echo $user->post->title;
}
;

user를 조회할 때, 치킨으로 시작하는 post를 같이 미리 로드 할 수 있습니다.

has()

has() 메소드를 이용하면, 해당 관계에서 최소 한개를 가지고 있는 결과를 조회합니다

User::has('posts')->get();을 사용하면 post를 한개라도 작성한 회원을 조회할것입니다.

쿼리로 표현한다면, 아래와 비슷할것 같습니다.

1
2
3
select *
from `user`
join `post` on `user`.`id` = `post`.`user_id`

has 메소드는 2번째 파라미터에 operator 와 3번째 파라미터에 count를 사용할 수 있습니다.

이 파라미터를 사용하면 예로 5개 이상의 글을 작성한 회원을 찾을 수 있습니다.
Eloquent를 사용하면 이와같이 작성 할 수 있습니다.

User::has('posts', '>=', 5)->get();

whereHas()

그렇다면 whereHas()는 뭐가 다를까요?

whereHas() 메소드는 두번째 파라미터로 콜백을 받아 더 복잡한 쿼리를 처리할 수 있습니다.

위와 같은 모델링에서 게시글의 제목이 치킨으로 시작하는 글을 작성한 회원을 찾는다면 아래와 같을것 입니다.

1
2
3
User::whereHas('posts', function ($query) {
$query->where('title', 'like', '치킨%');
})->get();

쿼리로 표현한다면

1
2
3
4
5
6
7
8
9
10
11
12
13
select *
from `user`
join `post` on
`user`.`id` = `post`.`user_id`
and
`post`.`title` like '치킨%'

OR

select *
from `user`
join `post` on `user`.`id` = `post`.`user_id`
where `post`.`title` like '치킨%'

이와 같이 표현할 수 있을것 같습니다.

참고자료

[Laravel]with() | has() | whereHas() 뭐가 다를까

https://blog.hodory.dev/2019/04/26/eloquent-orm-with-has-where-has/

Author

Hodory

Posted on

2019-04-27

Updated on

2022-08-11

Licensed under

댓글