Search code examples
spring-bootspring-data-jpa

Single row is inserted into with saveAllAndFlush


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": [
            {},
            {},
            {},
            {}
          ]
    }

Solution

  • 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.