I have this JSON which I want to store into DB using Spring Data JPA:
{
"loginName": "logv3iyynName",
"email": "eym3jvail",
"status": "ENABLED",
"roles": [
{
"roleCode": "ROLE_VIEW",
"systemType": "CRM"
},
{
"roleCode": "ROLE_VIEW",
"systemType": "CRM"
},
{
"roleCode": "ROLE_EDIT",
"systemType": "CRM"
},
{
"roleCode": "ROLE_EDIT",
"systemType": "CRM"
}
]
}
DTO object:
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserRequestDto {
private String loginName;
private String email;
private List<UserRoleDto> roles;
private UserStatus status;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserRoleDto {
private String roleCode;
private String systemType;
}
Hibernate entities:
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "users")
public class Users {
@Id
@GeneratedValue
private UUID id;
@Column(name = "login_name", unique = true, nullable = false)
private String loginName;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "email", unique = true, nullable = false)
private String email;
@Column(name = "status", nullable = false, length = 10)
@Enumerated(EnumType.STRING)
private UserStatus status;
}
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "roles")
public class Roles {
@Id
@Column(name = "user_id", nullable = false)
private UUID userId;
@Column(name = "role_code", nullable = false)
private String roleCode;
@Column(name = "system_type", nullable = false)
private String systemType;
}
I tried to insert the data using this code:
Users users = Users.builder()
.email(dto.getEmail())
.loginName(dto.getLoginName())
.fullName(dto.getFullName())
.password(dto.getPassword())
.status(dto.getStatus())
.build();
Users users1 = userRepository.saveAndFlush(users);
List<Roles> rolesList = dto.getRoles().stream()
.map(roleDto -> Roles.builder()
.userId(users1.getId())
.roleCode(roleDto.getRoleCode())
.systemType(roleDto.getSystemType())
.createdAt(OffsetDateTime.now())
.build())
.collect(Collectors.toList());
System.out.println("!!!!!!!!!!!!!!!! " + rolesList.size()); // I get here size 4
rolesRepository.saveAllAndFlush(rolesList);
I get always one inserted role into table roles
. I should insert 4 records using the above request. Do you know why I get a single role insert?
EDIT:
I tried this to get a user with a associated roles:
@Repository
public interface UserRepository extends JpaRepository<Users, UUID>, JpaSpecificationExecutor<Users> {
@EntityGraph(attributePaths = "roles")
Optional<Users> findById(UUID id); // Uses EntityGraph to fetch
roles eagerly
} // call this code from a endpoint
Updated entities:
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "users")
@NamedEntityGraph(
name = "Users.roles", // Name of the EntityGraph
attributeNodes = @NamedAttributeNode("roles") // Eagerly fetch the "roles" attribute
)
public class Users {
@Id
@GeneratedValue
private UUID id;
@Column(name = "login_name", unique = true, nullable = false)
private String loginName;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "email", unique = true, nullable = false)
private String email;
@Column(name = "status", nullable = false, length = 10)
@Enumerated(EnumType.STRING)
private UserStatus status;
}
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "roles")
public class Roles {
@Id
@Column(name = "user_id", nullable = false)
private UUID userId;
@Column(name = "role_code", nullable = false)
private String roleCode;
@Column(name = "system_type", nullable = false)
private String systemType;
}
As a result I get:
{
"loginName": "logv3iyynName",
"email": "eym3jvail",
"status": "ENABLED",
"roles": [
{},
{},
{},
{}
]
}
Update #1 OP claims that @EntityGraph
is not working. Please note that this was not in the original question, and it should strictly be asked as a new question. However
public interface UsersRepository extends JpaRepository<Users, UUID>, JpaSpecificationExecutor<Users> {
@EntityGraph(attributePaths = "roles")
Optional<Users> findById(UUID id);
}
is working according to the logs from test:
select
u1_0.id,
r1_0.user_id,
r1_0.id,
r1_0.role_code,
r1_0.system_type
from
users u1_0
left join
roles r1_0
on u1_0.id=r1_0.user_id
where
u1_0.id=?
Update #2 OP reports issues with saving, and gave link to this GitHub-repo. Repo does not contain any tests, and no description of how to reproduce the issue, however, I added this test, and it passes.
import com.profile.protection.admin.dto.UserRequestDto;
import com.profile.protection.admin.dto.UserRoleDto;
import com.profile.protection.admin.mapper.UserAssembler;
import com.profile.protection.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.testcontainers.containers.PostgreSQLContainer;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
@Import(UserServiceImpl.class)
class UserServiceImplTest {
@Autowired
UserService sut;
@MockitoBean
UserAssembler userAssembler; // not in use in UserServiceImpl, but required for autowiring
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@TestConfiguration
static class PostgresTestContainerConfig {
@Bean
@ServiceConnection
public PostgreSQLContainer<?> postgreSQLContainer() {
return new PostgreSQLContainer<>("postgres:15-alpine");
}
}
@Test
void create_givenValidDto_expectDbToBePopulated() {
// test-values are from OP's repo
var userRequestDto = UserRequestDto.builder()
.loginName("logv3ei64yynName")
.fullName("fu3lflb6Nyame")
.email("eym3jfv5abil")
.password("pvassword")
.roles(
List.of(
UserRoleDto.builder()
.roleCode("~roleCode1~")
.projectType("~projectType1~")
.build(),
UserRoleDto.builder()
.roleCode("~roleCode2~")
.projectType("~projectType2~")
.build()
)
)
.build();
// call create and assert
var userFromCreateAsOptional = sut.create(userRequestDto);
assertTrue(userFromCreateAsOptional.isPresent());
entityManager.clear();
var userInDb = userRepository.findById(userFromCreateAsOptional.get().getId()).orElseThrow();
assertEquals(2, userInDb.getRoles().size());
}
}
Extra dependencies
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:postgresql'
Update #3 Comment from OP
yesterday I managed to find a solution.
Without providing any additional explanation, I feel this leaves both the question and answers somewhat unresolved and unclear.
Original answer
I believe we want table definitions like this
CREATE TABLE users
(
id UUID primary key NOT NULL
-- other fields here
);
CREATE TABLE roles
(
id UUID primary key NOT NULL,
user_id UUID NOT NULL, -- FK back to table users
role_code VARCHAR(50),
system_type VARCHAR(50),
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id)
);
With the above table definitions and JPA,@OneToMany
may be used like this
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "users")
public class Users {
@Id
@GeneratedValue
private UUID id;
@OneToMany(mappedBy = "users", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Roles> roles = new HashSet<>();
// convenience method for adding child records
public void addRole(Roles rolesToAdd) {
rolesToAdd.setUsers(this);
this.roles.add(rolesToAdd);
}
}
And Roles
can be like this with a ref back to parent table.
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Setter
@Table(name = "roles")
public class Roles {
@Id
@GeneratedValue
private UUID id;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private Users users;
@Column(name = "role_code", nullable = false)
private String roleCode;
@Column(name = "system_type", nullable = false)
private String systemType;
}
When testing this (please note there's just a single call to save
)
var usersInTest = new Users();
usersInTest.addRole(Roles.builder()
.roleCode("~roleCode1~")
.systemType("~systemType1~")
.build());
usersInTest.addRole(Roles.builder()
.roleCode("~roleCode2~")
.systemType("~systemType2~")
.build());
usersRepository.save(usersInTest);
entityManager.flush();
var fetchedUser = usersRepository.findById(savedUser.getId()).orElseThrow();
// map roles to UserRoleDto
var userRoleDtos = fetchedUser.getRoles().stream()
.map(it -> new UserRoleDto(it.getRoleCode(), it.getSystemType()))
.toList();
assertEquals(2, userRoleDtos.size());
you will see one insert into table users, and two inserts into table roles.
The above code is verified using Spring Boot 3.4.1, Flyway and PostgreSQLContainer.