Hello Java Community.
Sorry if there’s any grammar / spelling mistakes in this post. I’ll try to convey my questions as clear as possible.
Introduction
I have basic knowledge about Java Programming. I took Programming Courses at my Uni and somewhat familiar with Java and OOP concepts in general ( barely ).
So for the last few weeks, I’ve been watching a great course from BouAli Free Code Camp Springboot Full Course on Youtube.
After completing the course, I challenge my self to create a basic E-Commerce Application with Springboot as the Framework, in order to practice my self with the knowledge i got from the course.
This Application will be a Simple Multi Tenant Application, where each tenant can create a product page, and a customer can purchase. Mind you i create this app to challenge my understanding about Springboot, and will not be a complex app for use.
Set Up
In the tutorial, there’s a usage of @ManyToMany
to map Many to Many Relationship. I thought that i could use Many To Many Mapping on Tenants <-> Users Relationship. In my understanding, A tenantEntity
could have multiple users
assigned, and multiple usersEntity
could belong to a tenantEntity
With that understanding, i try to create my tenantEntity
and userEntity
as follow
// tenantEntity.java
// Import Ommited for simplicity
// Using Lombok Setter, Getter, AllArgs, NoArgs
@Entity
@Table(name = "T_TENANT")
public class TenantEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(nullable = false)
private String tenantName;
@Column(length = 2000)
private String tenantDescription;
private String website;
@Column(nullable = false)
private Boolean isEnabled;
private Instant subscriptionExpiryDate;
@Column(length = 10)
private int gracePeriodDays;
@ManyToMany(mappedBy = "tenants", fetch = FetchType.LAZY)
private Set<UserEntity> users = new HashSet<>();
@Column(updatable = false, nullable = false)
@CreationTimestamp
private Instant createdAt;
@Column
@UpdateTimestamp
private Instant updatedAt;
}
// userEntity.java
// Import Ommited for simplicity
// Using Lombok Setter, Getter, AllArgs, NoArgs
@Entity
@Table(name = "T_USER")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String firstName;
private String lastName;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private boolean isEnabled;
@ManyToMany(cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
@JoinTable(
name = "t_users_tenants_mapping",
joinColumns = @JoinColumn(
name = "fk_user_id", referencedColumnName = "id"
),
inverseJoinColumns = @JoinColumn(
name = "fk_tenant_id", referencedColumnName = "id"
)
)
private Set<TenantEntity> tenants = new HashSet<>();
@Column(updatable = false, nullable = false)
@CreationTimestamp
private Instant createdAt;
@Column
@UpdateTimestamp
private Instant updatedAt;
}
Generating this relationship in the database
Creating New User
When creating new user, i could assign the Tenant to the User, resulting in these data
// POST api/v1/users
{
"username": "newuser01",
"first_name": "new",
"last_name": "user",
"email": "newuser@localhost",
"password": "<P@ssw0rd/>",
"is_enabled": true,
"tenant_id": [
"79cf0ecf-976a-472c-b250-2192e630a4e4",
"ac5b5786-c467-4dd6-b74d-7f70c83e1827"
]
}
fk_user_id|fk_tenant_id |
----------+------------------------------------+
6|79cf0ecf-976a-472c-b250-2192e630a4e4|
6|ac5b5786-c467-4dd6-b74d-7f70c83e1827|
Problem(s)
Now, how does one updates the data for the ManyToMany
relationship?
Say, i want to modify the membership of a user. i want to add new tenant or remove existing tenant?
I try creating update method on my userRepository
// userRepository
public interface UserEntityRepository extends JpaRepository<UserEntity, Long> {
@Transactional
@Modifying @Query("""
update UserEntity u set u.username = :username, u.email = :email, u.firstName = :firstName, u.lastName = :lastName, u.isEnabled = :isEnabled, u.tenants = :tenants, u.updatedAt = :updatedAt where u.id = :id""")
int updateUsernameAndEmailAndFirstNameAndLastNameAndIsEnabledAndTenantsAndUpdatedAtById(
@Param("username") String username,
@Param("email") String email,
@Param("firstName") String firstName,
@Param("lastName") String lastName,
@Param("isEnabled") boolean isEnabled,
@Param("tenants") Set<TenantEntity> tenants,
@Param("updatedAt") Instant updatedAt,
@Param("id") Long id
);
}
which being called by userService
public UserResponseDTO updateUser(Long userId, UserRequestDto dto){
Instant updateAt = Instant.now().atZone(ZoneId.of("Continent/Ciy")).toInstant();
Set<TenantEntity> tenantEntitySet = getTenantEntitiesFromUserRequestDTO(dto);
int updateRows = userEntityRepository.updateUsernameAndEmailAndFirstNameAndLastNameAndIsEnabledAndTenantsAndUpdatedAtById(
dto.username(),
dto.email(),
dto.first_name(),
dto.last_name(),
dto.is_enabled(),
tenantEntitySet,
updateAt,
userId
);
UserResponseDTO userResponseDTO;
UserEntity userEntity;
if (updateRows == 1) {
userEntity = userEntityRepository.findById(userId).orElse(new UserEntity());
} else {
userEntity = new UserEntity();
}
return userMapper.mapUserEntityToUserResponseDto(userEntity);
}
resulting in a NullPointerException
java.lang.NullPointerException: Cannot invoke "org.hibernate.sql.ast.tree.expression.Expression.getColumnReference()" because "pathSqlExpression" is null
in which resulted in a Hibernate Forum that states Updating *-to-many collections is not possible, but please submit a bug report to our issue tracker([https://hibernate.atlassian.net 14](https://hibernate.atlassian.net)) to improve the error message.
Questions
So what is the best practice of doing things with ManytoMany associations? I’ve found many tutorials that state the use of @ManyToMany Annotations, but left after they’re done inserting things to the database. How can i update the join table relations when the need to modify the data arises?
Is there a knowledge gap between me and Springboot especially with JPA? should i take another approach?
Any kind of feedback and suggestions are welcome
Thank You :D
Resources
- Free Code Camp | Springboot Full Course Youtube
- Baledung Explenation of ManyToMany
- vladmihalcea -> Just found out today, will read it later
- GeeksForGeeks ManyToMany