# Relationships: Has Many

> Users loaded with the tasks they made and the tasks assigned to them.

Goravel series, part 6 of 20. By Krishan Kumar. Published 2026-10-23.
Source: https://devchalk.com/goravel/06-relationships-has-many
Code: https://github.com/krishankumar01/goravel-taskflow/tree/part-06

In [Relationships: Belongs To](https://devchalk.com/goravel/05-relationships-belongs-to), each task loaded the user who made it and the user it is assigned to. In this part we go the other way, from a user to their tasks. We add a route that shows one user with their tasks, then a route that lists every user with their tasks, and use the query log to find and fix the N+1 problem.

Start from the Relationships: Belongs To code:

```bash
git clone https://github.com/krishankumar01/goravel-taskflow
cd goravel-taskflow
git checkout part-05
cp .env.example .env
go run . artisan key:generate
docker compose up -d
go run . artisan migrate
docker compose exec postgres psql -U goravel -d taskflow \
    -c "INSERT INTO users (name, email, password, role) VALUES ('Ana', 'ana@example.com', 'x', 'manager'), ('Ben', 'ben@example.com', 'x', 'member');" \
    -c "INSERT INTO tasks (created_by, assigned_to, title, description, due_at) VALUES (1, 2, 'Ship v1', 'The first release', '2026-11-02 09:00:00+00'), (1, NULL, 'Write the docs', 'Nobody has it yet', NULL);"
```

The three tasks you add below get ids 3 to 5, not 4 to 6.

## Has many relationships

A task belongs to a user because the task row holds the user's id. From the user's side, that same link is called [has many](https://www.goravel.dev/orm/relationships#one-to-many), because one user can have many tasks. A user has two such lists, the tasks they made and the tasks assigned to them. Add both:

```diff lang="go" title="app/models/user.go" collapse={1-13} t2:created=/(CreatedTasks) +/ t1:assigned=/(AssignedTasks) +/
 package models
 
 import (
 	"github.com/goravel/framework/database/orm"
 )
 
 type Role string
 
 const (
 	RoleManager Role = "manager"
 	RoleMember  Role = "member"
 )
 
 type User struct {
 	orm.Model
 	Name     string `gorm:"size:255;not null" json:"name"`
 	Email    string `gorm:"size:255;unique;not null" json:"email"`
 	Password string `gorm:"size:255;not null" json:"-"`
 	Role     Role   `gorm:"type:enum('manager','member');default:'member';not null" json:"role"`
+
+	CreatedTasks  []Task `gorm:"foreignKey:CreatedBy" json:"created_tasks"`
+	AssignedTasks []Task `gorm:"foreignKey:AssignedTo" json:"assigned_tasks"`
 }
```

- `CreatedTasks` holds the tasks the user made. Its `foreignKey:CreatedBy` names the field on `Task` that holds this user's id.
- `AssignedTasks` holds the tasks assigned to the user, linked by `AssignedTo`.
- Without `foreignKey`, GORM looks for a `UserID` field on `Task`. There is none, so every query that reads users fails, even `GET /tasks/1`.
- They are slices, not pointers like `Creator`, because a user can have many tasks.

There is no migration this time. The ids already live in the tasks table, so the users table needs nothing new.

> **Code** The code for every step of this part is in the tag [`part-06`](https://github.com/krishankumar01/goravel-taskflow/tree/part-06).

## Test data

Right now there are two tasks, and only one of them is assigned to anyone, so a list of users would not show much. Add a third user, Cleo, and three more tasks, all made by Ana. One of the tasks assigned to Ben is done. We use it later, when we leave done tasks out:

```bash
$ docker compose exec postgres psql -U goravel -d taskflow \
    -c "INSERT INTO users (name, email, password, role) VALUES ('Cleo', 'cleo@example.com', 'x', 'member');"
INSERT 0 1
$ docker compose exec postgres psql -U goravel -d taskflow \
    -c "INSERT INTO tasks (created_by, assigned_to, title, description, status) VALUES (1, 2, 'Fix the login bug', 'Users get logged out', 'in-progress'), (1, 3, 'Review the API', 'Check every endpoint', 'pending'), (1, 2, 'Plan the next release', 'Pick what goes in', 'done');"
INSERT 0 3
```

Three tasks are now assigned to Ben, and one to Cleo.

## Loading a has many relation

Add a route that loads a user with both lists:

```diff lang="go" title="routes/api.go" collapse={11-54} t2:created=/With."(CreatedTasks)"/ t1:assigned=/With."(AssignedTasks)"/
 package routes
 
 import (
 	"taskflow/app/facades"
 	"taskflow/app/models"
 
 	"github.com/goravel/framework/contracts/http"
 	"github.com/goravel/framework/contracts/route"
 )
 
 type CreateTaskRequest struct {
 	Title       string `form:"title" json:"title"`
 	Description string `form:"description" json:"description"`
 }
 
 func Api() {
 	facades.Route().Prefix("api/v1").Group(func(router route.Router) {
 		router.Post("tasks", func(ctx http.Context) http.Response {
 			var request CreateTaskRequest
 			if err := ctx.Request().Bind(&request); err != nil {
 				return ctx.Response().Json(400, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, request)
 		}).Name("tasks.create")
 
 		router.Get("tasks/{id}", func(ctx http.Context) http.Response {
 			var task models.Task
 			err := facades.Orm().Query().
 				With("Creator").
 				With("Assignee").
 				Find(&task, ctx.Request().RouteInt("id"))
 			if err != nil {
 				return ctx.Response().Json(500, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, task)
 		})
 
 		router.Get("tasks", func(ctx http.Context) http.Response {
 			status := ctx.Request().Query("status")
 			page := ctx.Request().QueryInt("page", 1)
 			filters := ctx.Request().QueryMap("filters")
 
 			return ctx.Response().Json(200, map[string]any{
 				"status":  status,
 				"page":    page,
 				"filters": filters,
 			})
 		})
+
+		router.Get("users/{id}", func(ctx http.Context) http.Response {
+			var user models.User
+			err := facades.Orm().Query().
+				With("CreatedTasks").
+				With("AssignedTasks").
+				Find(&user, ctx.Request().RouteInt("id"))
+			if err != nil {
+				return ctx.Response().Json(500, map[string]any{
+					"error": err.Error(),
+				})
+			}
+
+			return ctx.Response().Json(200, user)
+		})
 	})
 }
```

Add a request for Ben, user 2:

```diff lang="http" title="requests.http" collapse={1-9}
 ### Create task
 POST http://localhost:3000/api/v1/tasks
 Content-Type: application/json
 
 {
   "title": "Ship v1",
   "description": "The first release"
 }
 
 ### Show task
 GET http://localhost:3000/api/v1/tasks/1
 
 ### List tasks
 GET http://localhost:3000/api/v1/tasks?status=done&page=2&filters[priority]=high
 
+### Show user
+GET http://localhost:3000/api/v1/users/2
+
 ### Hello
 GET http://localhost:3000/hello
```

If `air` is not running, start it, as in [Routing & Response](https://devchalk.com/goravel/02-routing-response#hot-reload-with-air). Then send Show user. Here is the response, formatted:

```json title="GET /api/v1/users/2" collapse={23-48} t2:created=/"(created_tasks)":/ t1:assigned=/"(assigned_tasks)":/
{
  "created_at": null,
  "updated_at": null,
  "id": 2,
  "name": "Ben",
  "email": "ben@example.com",
  "role": "member",
  "created_tasks": [],
  "assigned_tasks": [
    {
      "created_at": null,
      "updated_at": null,
      "id": 1,
      "created_by": 1,
      "creator": null,
      "assigned_to": 2,
      "assignee": null,
      "title": "Ship v1",
      "description": "The first release",
      "status": "pending",
      "due_at": "2026-11-02 09:00:00"
    },
    {
      "created_at": null,
      "updated_at": null,
      "id": 4,
      "created_by": 1,
      "creator": null,
      "assigned_to": 2,
      "assignee": null,
      "title": "Fix the login bug",
      "description": "Users get logged out",
      "status": "in-progress",
      "due_at": null
    },
    {
      "created_at": null,
      "updated_at": null,
      "id": 6,
      "created_by": 1,
      "creator": null,
      "assigned_to": 2,
      "assignee": null,
      "title": "Plan the next release",
      "description": "Pick what goes in",
      "status": "done",
      "due_at": null
    }
  ]
}
```

Ben made no tasks, so `created_tasks` is an empty list. He has three in `assigned_tasks`. Inside each task, `creator` and `assignee` are `null`, because this route does not load them.

The [query log](https://devchalk.com/goravel/05-relationships-belongs-to#the-query-log) shows one query for the user and one for each list:

```text wrap t2:created=/"(created_by)" = 2/ t1:assigned=/"(assigned_to)" = 2/
[2026-10-23 07:49:41.261] local.info: [1.316ms] [rows:3] SELECT * FROM "tasks" WHERE "tasks"."assigned_to" = 2
[2026-10-23 07:49:41.262] local.info: [0.691ms] [rows:0] SELECT * FROM "tasks" WHERE "tasks"."created_by" = 2
[2026-10-23 07:49:41.262] local.info: [6.399ms] [rows:1] SELECT * FROM "users" WHERE "users"."id" = 2
```

## Loading relations for a list

Add a route that lists every user. Leave out `With` for now, to see what an unloaded list looks like:

```diff lang="go" title="routes/api.go" collapse={11-54} collapse={68-83}
 package routes
 
 import (
 	"taskflow/app/facades"
 	"taskflow/app/models"
 
 	"github.com/goravel/framework/contracts/http"
 	"github.com/goravel/framework/contracts/route"
 )
 
 type CreateTaskRequest struct {
 	Title       string `form:"title" json:"title"`
 	Description string `form:"description" json:"description"`
 }
 
 func Api() {
 	facades.Route().Prefix("api/v1").Group(func(router route.Router) {
 		router.Post("tasks", func(ctx http.Context) http.Response {
 			var request CreateTaskRequest
 			if err := ctx.Request().Bind(&request); err != nil {
 				return ctx.Response().Json(400, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, request)
 		}).Name("tasks.create")
 
 		router.Get("tasks/{id}", func(ctx http.Context) http.Response {
 			var task models.Task
 			err := facades.Orm().Query().
 				With("Creator").
 				With("Assignee").
 				Find(&task, ctx.Request().RouteInt("id"))
 			if err != nil {
 				return ctx.Response().Json(500, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, task)
 		})
 
 		router.Get("tasks", func(ctx http.Context) http.Response {
 			status := ctx.Request().Query("status")
 			page := ctx.Request().QueryInt("page", 1)
 			filters := ctx.Request().QueryMap("filters")
 
 			return ctx.Response().Json(200, map[string]any{
 				"status":  status,
 				"page":    page,
 				"filters": filters,
 			})
 		})
 
+		router.Get("users", func(ctx http.Context) http.Response {
+			var users []models.User
+			err := facades.Orm().Query().Find(&users)
+			if err != nil {
+				return ctx.Response().Json(500, map[string]any{
+					"error": err.Error(),
+				})
+			}
+
+			return ctx.Response().Json(200, users)
+		})
+
 		router.Get("users/{id}", func(ctx http.Context) http.Response {
 			var user models.User
 			err := facades.Orm().Query().
 				With("CreatedTasks").
 				With("AssignedTasks").
 				Find(&user, ctx.Request().RouteInt("id"))
 			if err != nil {
 				return ctx.Response().Json(500, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, user)
 		})
 	})
 }
```

Add a request for it:

```diff lang="http" title="requests.http" collapse={1-9}
 ### Create task
 POST http://localhost:3000/api/v1/tasks
 Content-Type: application/json
 
 {
   "title": "Ship v1",
   "description": "The first release"
 }
 
 ### Show task
 GET http://localhost:3000/api/v1/tasks/1
 
 ### List tasks
 GET http://localhost:3000/api/v1/tasks?status=done&page=2&filters[priority]=high
 
+### List users
+GET http://localhost:3000/api/v1/users
+
 ### Show user
 GET http://localhost:3000/api/v1/users/2
 
 ### Hello
 GET http://localhost:3000/hello
```

Send List users. Here is the response, formatted:

```json title="GET /api/v1/users"
[
  {
    "created_at": null,
    "updated_at": null,
    "id": 1,
    "name": "Ana",
    "email": "ana@example.com",
    "role": "manager",
    "created_tasks": null,
    "assigned_tasks": null
  },
  {
    "created_at": null,
    "updated_at": null,
    "id": 2,
    "name": "Ben",
    "email": "ben@example.com",
    "role": "member",
    "created_tasks": null,
    "assigned_tasks": null
  },
  {
    "created_at": null,
    "updated_at": null,
    "id": 3,
    "name": "Cleo",
    "email": "cleo@example.com",
    "role": "member",
    "created_tasks": null,
    "assigned_tasks": null
  }
]
```

Every list is `null`, which means it was not loaded. `[]`, like Ben's `created_tasks` above, means it was loaded and is empty. As with `Creator`, reading `user.AssignedTasks` later does not fetch it. If you forget `With`, you get `null` and no error.

### The N+1 problem

Goravel has `Load`, which loads a relation onto a model you already have. Calling it in a loop is a common first try. Load the assigned tasks, the work each user has to do:

```diff lang="go" title="routes/api.go" collapse={11-55} collapse={75-91}
 package routes
 
 import (
 	"taskflow/app/facades"
 	"taskflow/app/models"
 
 	"github.com/goravel/framework/contracts/http"
 	"github.com/goravel/framework/contracts/route"
 )
 
 type CreateTaskRequest struct {
 	Title       string `form:"title" json:"title"`
 	Description string `form:"description" json:"description"`
 }
 
 func Api() {
 	facades.Route().Prefix("api/v1").Group(func(router route.Router) {
 		router.Post("tasks", func(ctx http.Context) http.Response {
 			var request CreateTaskRequest
 			if err := ctx.Request().Bind(&request); err != nil {
 				return ctx.Response().Json(400, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, request)
 		}).Name("tasks.create")
 
 		router.Get("tasks/{id}", func(ctx http.Context) http.Response {
 			var task models.Task
 			err := facades.Orm().Query().
 				With("Creator").
 				With("Assignee").
 				Find(&task, ctx.Request().RouteInt("id"))
 			if err != nil {
 				return ctx.Response().Json(500, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, task)
 		})
 
 		router.Get("tasks", func(ctx http.Context) http.Response {
 			status := ctx.Request().Query("status")
 			page := ctx.Request().QueryInt("page", 1)
 			filters := ctx.Request().QueryMap("filters")
 
 			return ctx.Response().Json(200, map[string]any{
 				"status":  status,
 				"page":    page,
 				"filters": filters,
 			})
 		})
 
 		router.Get("users", func(ctx http.Context) http.Response {
 			var users []models.User
 			err := facades.Orm().Query().Find(&users)
 			if err != nil {
 				return ctx.Response().Json(500, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
+			for i := range users {
+				if err := facades.Orm().Query().Load(&users[i], "AssignedTasks"); err != nil {
+					return ctx.Response().Json(500, map[string]any{
+						"error": err.Error(),
+					})
+				}
+			}
+
 			return ctx.Response().Json(200, users)
 		})
 
 		router.Get("users/{id}", func(ctx http.Context) http.Response {
 			var user models.User
 			err := facades.Orm().Query().
 				With("CreatedTasks").
 				With("AssignedTasks").
 				Find(&user, ctx.Request().RouteInt("id"))
 			if err != nil {
 				return ctx.Response().Json(500, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, user)
 		})
 	})
 }
```

Send List users again. It works, but look at the log:

```text wrap
[2026-10-23 07:50:15.892] local.info: [5.191ms] [rows:3] SELECT * FROM "users"
[2026-10-23 07:50:15.894] local.info: [1.312ms] [rows:0] SELECT * FROM "tasks" WHERE "tasks"."assigned_to" = 1
[2026-10-23 07:50:15.894] local.info: [2.469ms] [rows:1] SELECT * FROM "users" WHERE "users"."id" = 1
[2026-10-23 07:50:15.896] local.info: [0.738ms] [rows:3] SELECT * FROM "tasks" WHERE "tasks"."assigned_to" = 2
[2026-10-23 07:50:15.896] local.info: [1.474ms] [rows:1] SELECT * FROM "users" WHERE "users"."id" = 2
[2026-10-23 07:50:15.897] local.info: [0.528ms] [rows:1] SELECT * FROM "tasks" WHERE "tasks"."assigned_to" = 3
[2026-10-23 07:50:15.897] local.info: [1.150ms] [rows:1] SELECT * FROM "users" WHERE "users"."id" = 3
```

Seven queries for three users. This is called the N+1 problem. There is one query for the list, and then a separate load for each of the N users.

Here each user costs two queries, because `Load` reads the user again, together with their tasks. With 100 users, this would be 201 queries for one request.

### Eager loading a list

Take the loop out and use `With` on the list, as in [Relationships: Belongs To](https://devchalk.com/goravel/05-relationships-belongs-to#eager-loading-with-with):

```diff lang="go" title="routes/api.go" collapse={11-55} collapse={78-94} t1:assigned=/With."(AssignedTasks)"/
 package routes
 
 import (
 	"taskflow/app/facades"
 	"taskflow/app/models"
 
 	"github.com/goravel/framework/contracts/http"
 	"github.com/goravel/framework/contracts/route"
 )
 
 type CreateTaskRequest struct {
 	Title       string `form:"title" json:"title"`
 	Description string `form:"description" json:"description"`
 }
 
 func Api() {
 	facades.Route().Prefix("api/v1").Group(func(router route.Router) {
 		router.Post("tasks", func(ctx http.Context) http.Response {
 			var request CreateTaskRequest
 			if err := ctx.Request().Bind(&request); err != nil {
 				return ctx.Response().Json(400, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, request)
 		}).Name("tasks.create")
 
 		router.Get("tasks/{id}", func(ctx http.Context) http.Response {
 			var task models.Task
 			err := facades.Orm().Query().
 				With("Creator").
 				With("Assignee").
 				Find(&task, ctx.Request().RouteInt("id"))
 			if err != nil {
 				return ctx.Response().Json(500, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, task)
 		})
 
 		router.Get("tasks", func(ctx http.Context) http.Response {
 			status := ctx.Request().Query("status")
 			page := ctx.Request().QueryInt("page", 1)
 			filters := ctx.Request().QueryMap("filters")
 
 			return ctx.Response().Json(200, map[string]any{
 				"status":  status,
 				"page":    page,
 				"filters": filters,
 			})
 		})
 
 		router.Get("users", func(ctx http.Context) http.Response {
 			var users []models.User
-			err := facades.Orm().Query().Find(&users)
+			err := facades.Orm().Query().
+				With("AssignedTasks").
+				Find(&users)
 			if err != nil {
 				return ctx.Response().Json(500, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
-			for i := range users {
-				if err := facades.Orm().Query().Load(&users[i], "AssignedTasks"); err != nil {
-					return ctx.Response().Json(500, map[string]any{
-						"error": err.Error(),
-					})
-				}
-			}
-
 			return ctx.Response().Json(200, users)
 		})
 
 		router.Get("users/{id}", func(ctx http.Context) http.Response {
 			var user models.User
 			err := facades.Orm().Query().
 				With("CreatedTasks").
 				With("AssignedTasks").
 				Find(&user, ctx.Request().RouteInt("id"))
 			if err != nil {
 				return ctx.Response().Json(500, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, user)
 		})
 	})
 }
```

Send List users again. Ana has no tasks, Ben has three and Cleo has one, the same as with the loop. The log has two lines:

```text wrap t1:assigned=/"(assigned_to)" IN/
[2026-10-23 07:50:29.602] local.info: [1.504ms] [rows:4] SELECT * FROM "tasks" WHERE "tasks"."assigned_to" IN (1,2,3)
[2026-10-23 07:50:29.602] local.info: [7.835ms] [rows:3] SELECT * FROM "users"
```

One query reads the users. The other reads the tasks of all of them at once, with `assigned_to IN (1,2,3)`, and GORM hands each task to its user. It stays two queries however many users there are.

## Filtered and nested loads

`With` can do more than load a whole relation. It can leave some rows out, and it can load relations of the loaded rows. A done task is no longer work to do, so leave done tasks out. Then load who made each task:

```diff lang="go" title="routes/api.go" collapse={11-56} collapse={74-90}
 package routes
 
 import (
 	"taskflow/app/facades"
 	"taskflow/app/models"
 
+	"github.com/goravel/framework/contracts/database/orm"
 	"github.com/goravel/framework/contracts/http"
 	"github.com/goravel/framework/contracts/route"
 )
 
 type CreateTaskRequest struct {
 	Title       string `form:"title" json:"title"`
 	Description string `form:"description" json:"description"`
 }
 
 func Api() {
 	facades.Route().Prefix("api/v1").Group(func(router route.Router) {
 		router.Post("tasks", func(ctx http.Context) http.Response {
 			var request CreateTaskRequest
 			if err := ctx.Request().Bind(&request); err != nil {
 				return ctx.Response().Json(400, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, request)
 		}).Name("tasks.create")
 
 		router.Get("tasks/{id}", func(ctx http.Context) http.Response {
 			var task models.Task
 			err := facades.Orm().Query().
 				With("Creator").
 				With("Assignee").
 				Find(&task, ctx.Request().RouteInt("id"))
 			if err != nil {
 				return ctx.Response().Json(500, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, task)
 		})
 
 		router.Get("tasks", func(ctx http.Context) http.Response {
 			status := ctx.Request().Query("status")
 			page := ctx.Request().QueryInt("page", 1)
 			filters := ctx.Request().QueryMap("filters")
 
 			return ctx.Response().Json(200, map[string]any{
 				"status":  status,
 				"page":    page,
 				"filters": filters,
 			})
 		})
 
 		router.Get("users", func(ctx http.Context) http.Response {
 			var users []models.User
 			err := facades.Orm().Query().
-				With("AssignedTasks").
+				With("AssignedTasks", func(query orm.Query) orm.Query {
+					return query.Where("status <> ?", models.StatusDone)
+				}).
+				With("AssignedTasks.Creator").
 				Find(&users)
 			if err != nil {
 				return ctx.Response().Json(500, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, users)
 		})
 
 		router.Get("users/{id}", func(ctx http.Context) http.Response {
 			var user models.User
 			err := facades.Orm().Query().
 				With("CreatedTasks").
 				With("AssignedTasks").
 				Find(&user, ctx.Request().RouteInt("id"))
 			if err != nil {
 				return ctx.Response().Json(500, map[string]any{
 					"error": err.Error(),
 				})
 			}
 
 			return ctx.Response().Json(200, user)
 		})
 	})
 }
```

- The function given to `With` filters what it loads. Here it leaves out tasks whose status is done.
- `AssignedTasks.Creator` loads each task's creator too. The dot goes one relation deeper.

Send List users again. The done task, "Plan the next release", is gone, and each task has its creator.

```json title="GET /api/v1/users" collapse={43-64} collapse={67-99}
[
  {
    "created_at": null,
    "updated_at": null,
    "id": 1,
    "name": "Ana",
    "email": "ana@example.com",
    "role": "manager",
    "created_tasks": null,
    "assigned_tasks": []
  },
  {
    "created_at": null,
    "updated_at": null,
    "id": 2,
    "name": "Ben",
    "email": "ben@example.com",
    "role": "member",
    "created_tasks": null,
    "assigned_tasks": [
      {
        "created_at": null,
        "updated_at": null,
        "id": 1,
        "created_by": 1,
        "creator": {
          "created_at": null,
          "updated_at": null,
          "id": 1,
          "name": "Ana",
          "email": "ana@example.com",
          "role": "manager",
          "created_tasks": null,
          "assigned_tasks": null
        },
        "assigned_to": 2,
        "assignee": null,
        "title": "Ship v1",
        "description": "The first release",
        "status": "pending",
        "due_at": "2026-11-02 09:00:00"
      },
      {
        "created_at": null,
        "updated_at": null,
        "id": 4,
        "created_by": 1,
        "creator": {
          "created_at": null,
          "updated_at": null,
          "id": 1,
          "name": "Ana",
          "email": "ana@example.com",
          "role": "manager",
          "created_tasks": null,
          "assigned_tasks": null
        },
        "assigned_to": 2,
        "assignee": null,
        "title": "Fix the login bug",
        "description": "Users get logged out",
        "status": "in-progress",
        "due_at": null
      }
    ]
  },
  {
    "created_at": null,
    "updated_at": null,
    "id": 3,
    "name": "Cleo",
    "email": "cleo@example.com",
    "role": "member",
    "created_tasks": null,
    "assigned_tasks": [
      {
        "created_at": null,
        "updated_at": null,
        "id": 5,
        "created_by": 1,
        "creator": {
          "created_at": null,
          "updated_at": null,
          "id": 1,
          "name": "Ana",
          "email": "ana@example.com",
          "role": "manager",
          "created_tasks": null,
          "assigned_tasks": null
        },
        "assigned_to": 3,
        "assignee": null,
        "title": "Review the API",
        "description": "Check every endpoint",
        "status": "pending",
        "due_at": null
      }
    ]
  }
]
```

Three queries this time:

```text wrap
[2026-10-23 07:50:47.458] local.info: [0.760ms] [rows:1] SELECT * FROM "users" WHERE "users"."id" = 1
[2026-10-23 07:50:47.458] local.info: [4.612ms] [rows:3] SELECT * FROM "tasks" WHERE "tasks"."assigned_to" IN (1,2,3) AND status <> 'done'
[2026-10-23 07:50:47.458] local.info: [9.377ms] [rows:3] SELECT * FROM "users"
```

The filter is part of the tasks query. All the tasks were made by Ana, so one more query, for user 1, loads every creator. With more creators it is still one query, using `IN`.

---

Previous: [05. Relationships: Belongs To](https://devchalk.com/goravel/05-relationships-belongs-to.md)
Next: [07. Factories & Seeding](https://devchalk.com/goravel/07-factories-seeding.md)
Series: [Goravel](https://devchalk.com/goravel.md)
