SpringData N+1 solution with @NamedEntityGraph

Paul Ravvich
Hibernate At the Gates of Mastery
4 min readFeb 25, 2024

--

In the development of Java applications, optimizing database queries plays a crucial role, especially when working with JPA. The @NamedEntityGraph annotation from the JPA 2.1 specification offers a solution for managing entity selections and their associations, allowing necessary data to be loaded in a single query and thereby avoiding the N+1 query problem.

Hi, this is Paul, and welcome to this article, we explore how @NamedEntityGraph work and how we can solve the N+1 issue with it.

Let’s consider an example. We work with three main entities: User, Role, and Group. These entities represent typical components of an access management system, where:

  • User describes a system user with all their attributes and connections.
  • Role represents a role or permissions that can be assigned to a user. One user can have many roles, meaning the type of relationship is ManyToMany.
  • Group defines a group to which a user can belong. One user can belong to many groups, meaning the type of relationship is ManyToMany.

This is roughly what the mapping through a junction table would look like:

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "users_roles",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
@ToString.Exclude
private Set<Role> roles;

Depending on the context, we may need to load a user with their roles, groups, or both together. This is where @NamedEntityGraph comes in handy. For example, when querying a user by ID, we need to join roles, when querying by username, join only roles, and when querying a list with pagination, join both roles and groups simultaneously.

How to ensure such variability?

Here is an example of a repository.

@Repository
@Transactional
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {

@NotNull
@EntityGraph(value = "user[roles]")
Optional<User> findById(@NotNull Long userId);

@EntityGraph(value = "user[groups]")
User findByUsername(@NonNull String username);

@NotNull
@Override
@EntityGraph(value = "user[roles][groups]")
Page<User> findAll(@Nullable Specification<User> spec, @NotNull Pageable pageable);

Here’s what the DB schema will look like:

CREATE TABLE IF NOT EXISTS users
(
id BIGSERIAL PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
);

CREATE TABLE IF NOT EXISTS roles
(
id BIGINT PRIMARY KEY,
title VARCHAR(255) UNIQUE NOT NULL,
system_name VARCHAR(255) UNIQUE NOT NULL
);

CREATE TABLE IF NOT EXISTS users_roles
(
user_id BIGINT NOT NULL REFERENCES users (id),
role_id BIGINT NOT NULL REFERENCES roles (id)
);

CREATE UNIQUE INDEX IF NOT EXISTS ui_user_role ON users_roles (user_id, role_id);

CREATE TABLE IF NOT EXISTS groups
(
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP NOT NULL
);

CREATE TABLE IF NOT EXISTS users_groups
(
user_id BIGINT REFERENCES users (id),
group_id BIGINT REFERENCES groups (id)
);

CREATE UNIQUE INDEX IF NOT EXISTS ui_user_group ON users_roles (user_id, group_id);

Java Entity


@Entity
@Getter
@Setter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "users")
@NamedEntityGraphs({
@NamedEntityGraph(
name = "user[roles]",
attributeNodes = {
@NamedAttributeNode("roles")
}),
@NamedEntityGraph(
name = "user[groups]",
attributeNodes = {
@NamedAttributeNode("groups"),
}),
@NamedEntityGraph(
name = "user[roles][groups]",
attributeNodes = {
@NamedAttributeNode("roles"),
@NamedAttributeNode("groups")
})
})
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "username")
private String username;

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "users_roles",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
@ToString.Exclude
private Set<Role> roles;

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "users_groups",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "group_id", referencedColumnName = "id"))
@ToString.Exclude
private Set<Group> groups;

// equals and hashCode
}

@Getter
@Setter
@Entity
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "roles")
public class Role {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "name")
private String name;

@ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
@ToString.Exclude
private List<User> users;

// equals and hashCode
}

@Entity
@Getter
@Setter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "groups")
public class Group {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "name")
private String name;

@ManyToMany(mappedBy = "groups", fetch = FetchType.LAZY)
@ToString.Exclude
private Set<User> users;

// equals and hashCode
}

The greatest interest for us is this plot:

@NamedEntityGraphs({
@NamedEntityGraph(
name = "user[roles]",
attributeNodes = {
@NamedAttributeNode("roles")
}),
@NamedEntityGraph(
name = "user[groups]",
attributeNodes = {
@NamedAttributeNode("groups"),
}),
@NamedEntityGraph(
name = "user[roles][groups]",
attributeNodes = {
@NamedAttributeNode("roles"),
@NamedAttributeNode("groups")
})
})
public class User ...

@NamedAttributeNode(“roles”) the “roles” here is the name of the field.

private Set<Role> roles;

Here we define which names for joining which related entities we will use, thus name = “user[groups]” is the key for joining the roles table for use in the repository. Each element of attributeNodes the array turns into an SQL JOIN operation. To apply just add @EntityGraph(value = “user[roles]”) the above repository method:

@EntityGraph(value = "user[roles]")
Optional<User> findById(@NotNull Long userId);

And so on for the other named graphs. It should be noted that there is no strict convention for naming, and you can name your graph whatever you want or come up with your approach, although user[roles] or user.roles is most often used.

As a result, you get a join instead of additional queries. This is useful for searching for individual composite objects, but it has the greatest impact on list queries, as the N+1 problem is particularly unpleasant there. We are not analyzing generated SQL queries here because it’s just a typical JOIN operation but in real life, you have to check this using configuration:

spring:
jpa:
properties:
hibernate:
format_sql: 'true'
show_sql: 'true'

It helps you make sure you're Entity Graph works correctly.

Conclusion

Thus, @NamedEntityGraph and @EntityGraph offer flexible and powerful tools for managing the loading of related data in JPA, allowing you to significantly improve the efficiency of queries and the overall performance of the application.

Thank you for reading until the end. Before you go:

--

--

Paul Ravvich
Hibernate At the Gates of Mastery

Software Engineer with over 10 years of XP. Join me for tips on Programming, System Design, and productivity in tech! New articles every Tuesday and Thursday!