Should We Use Set or List in Relationships When Using Spring Data JPA?

Sinan Alagöz
5 min readMay 1, 2023

--

In this tutorial, I explain the selection List or Set collection when managing Associations on Spring Data JPA. Firstly, you should know this is more about your requirements, situations...

What happens If I use Set instead of List?

If you use HashSet, you lose control over the sequence. Because HashSet doesn’t guarantee item order in the Collection. But you can overcome this problem. You have 2 options to overcome;

  • You can use the @OrderBy annotation. By adding this annotation, you can ensure that SQL queries contain the “ORDER BY” statement. So you can get the sorted data. Example of using OrderBy annotation;
@ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
@JoinTable(name = "user_course",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
@OrderBy("name desc")
private List<Course> courses = new ArrayList<>();

To use this annotation, you must add the @OrderBy annotation and you can specify which property you want it to sort according to.

  • You can use @OrderColumn to solve the problem. The @OrderColumn annotation creates an extra column to maintain the order of the inserted data. Using this column, it keeps the order of the inserted data. For example “course_order” column.
@ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
@JoinTable(name = "user_course",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
@OrderColumn
private List<Course> courses = new ArrayList<>();
order-column-database-table

If Using a Set in JPA Entity Causes Some Problems, Why Should I Use It?

Short answer, If you build ManyToMany relationships using List, you will incur unnecessary costs when you want to delete them.

What I mean is, for example, we have a user named User1 and 4 courses named Course1, Course2, Course3, and Course4. Let’s assume that User1 is enrolled in all of these courses. If User1 returns Course2 for any reason. We need to remove the relationship between Course2 and User1 from the database.

@Entity
@Data
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;

private String name;

@ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
@JoinTable(name = "user_course",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses = new ArrayList<>();

public void addCourse(Course course) {
courses.add(course);
course.getUsers().add(this);
}

public void deleteCourse(Course course) {
courses.remove(course);
course.getUsers().remove(this);
}
}
@Entity
@Getter
@Setter
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;

private String name;

@ManyToMany(mappedBy = "courses")
private List<User> users = new ArrayList<>();
}

We have two entities named User and Course. And I created four courses and one user.

courses
users

Since we have a ManyToMany relationship, we have a junction table. In the first case, it is empty.

junction-table

Let’s add 4 courses to our user.

junction-table-with-records

So far, no problem. The problem will occur when we start deleting courses from the user. I have an endpoint to delete the course like this;

@DeleteMapping("/users/{userId}/courses/{courseId}")
public boolean deleteUser(@PathVariable UUID userId, @PathVariable UUID courseId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("user not found"));
Course course = courseRepository.findById(courseId)
.orElseThrow(() -> new RuntimeException("course not found"));
user.deleteCourse(course);
userRepository.save(user);
return true;
}

If we delete Course2 from User1, the following queries will run;

Hibernate: select user0_.id as id1_3_0_, user0_.name as name2_3_0_ from users user0_ where user0_.id=?
Hibernate: select course0_.id as id1_1_0_, course0_.name as name2_1_0_ from courses course0_ where course0_.id=?
Hibernate: select courses0_.user_id as user_id1_2_0_, courses0_.course_id as course_i2_2_0_, course1_.id as id1_1_1_, course1_.name as name2_1_1_ from user_course courses0_ inner join courses course1_ on courses0_.course_id=course1_.id where courses0_.user_id=?
Hibernate: select users0_.course_id as course_i2_2_0_, users0_.user_id as user_id1_2_0_, user1_.id as id1_3_1_, user1_.name as name2_3_1_ from user_course users0_ inner join users user1_ on users0_.user_id=user1_.id where users0_.course_id=?
// The following are more important
Hibernate: delete from user_course where user_id=?
Hibernate: insert into user_course (user_id, course_id) values (?, ?)
Hibernate: insert into user_course (user_id, course_id) values (?, ?)
Hibernate: insert into user_course (user_id, course_id) values (?, ?)

When we delete a course from a user who has 4 courses, hibernate first deletes all the courses the user has and then adds back the courses (Course1, Course3, Course4) that should not be deleted.

This is why it might be a better idea to use Set in ManyToMany relationships.

What is the advantage of using List instead of Set?

We have already said that it is more advantageous to use Set in ManyToMany relationships. But this is not true for all cases. If you look at the Hibernate documentation, you can see a section called “Bags and lists are the most efficient inverse collections”. The code shown as an example in the document is as follows;

Parent p = (Parent) sess.load(Parent.class, id);
Child c = new Child();
c.setParent(p);
p.getChildren().add(c); //no need to fetch the collection!
sess.flush();

I added the following code to make it more understandable for our system.

@Entity
@Getter
@Setter
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;

private String name;

@ManyToMany(mappedBy = "courses")
private List<User> users = new ArrayList<>();

// new relationship
@OneToMany(
mappedBy = "course",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Campaign> campaigns = new ArrayList<>();
}
@Entity
@Getter
@Setter
public class Campaign {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;

private String name;

@ManyToOne(fetch = FetchType.LAZY)
private Course course;
}
Course course = courseRepository.findAll().get(0);

Campaign campaign = new Campaign();
campaign.setName("Example Campaign");
campaign.setCourse(course);
course.getCampaigns().add(campaign); //no need to fetch the collection!

courseRepository.save(course);
  • If the course.getCampaigns() method returns a List, we don’t need to get the value of this list for the add() or addAll() methods. Because the list can contain duplicate data, we don’t need to check for duplicate data.
  • If the course.getCampaigns() method returns a Set, We pull campaigns because there need to be extra checks for things like data replication.

This is why it might be a better idea to use List in OneToMany relationships.

Summary

In short, I think it might be a better idea to use Set in ManyToMany relationships and List in OneToMany relationships. But I would like to point out that this situation is more related to your requirements.

--

--

Sinan Alagöz
Sinan Alagöz

Written by Sinan Alagöz

I have been working as a software engineer for 4 years. I try to improve myself to become a better developer. I try to share what I have learned here.

No responses yet