【问题标题】:SQl error - Referential integrity constraint violation when persisting several entities at onceSQl 错误 - 一次持久化多个实体时违反参照完整性约束
【发布时间】:2021-04-03 23:47:29
【问题描述】:

我正在尝试解决的问题

我正在尝试对UserRole 之间的@ManyToMany 关系建模,这样一个用户可以拥有n 个角色,并且一个角色被多个用户引用。一个角色即使没有被任何用户引用(分离)也可以被持久化,并且也允许没有角色的用户。

RoleResourcePermission之间必须建立同一种关系。

让您了解每个实体的外观:

  • ResourcePermissionRole 都有一组有限的值。例如,如果Patient 恰好是一个资源,那么一个资源权限可能是"PATIENT:READ""PATIENT:WRITE",而角色DOCTOR 拥有其中的几个权限。我希望到目前为止我的数据模型看起来如何是清楚的。

我在用什么

  • 目前,我正在使用spring-data-jpa 版本2.4.2 对我的实体进行建模,并创建我的CRUD 存储库。除了基本路径和媒体类型,我没有任何具体的配置(全部设置为默认)。
  • Hibernate 是我的持久化提供程序 atm。
  • 关于我的数据源,我在我的开发环境中使用内存中的 H2,同样也没有特定的配置。

我是如何解决的

这是我的实体的样子

User.java

@Table
@Entity
@Data
public class User implements Serializable {

    private static final long serialVersionUID = 1123146940559321847L;

    @Id
    @GeneratedValue(generator = "user-id-generator")
    @GenericGenerator(name = "user-id-generator",
            strategy = "....security.entity.UserIdGenerator",
            parameters = @Parameter(name = "prefix", value = "USER-")
    )
    @Column(unique = true, nullable = false)
    private String id;


    @Column
    private int age;
    @Column(unique = true, nullable = false)
    private String username;
    @Column(unique = false, nullable = false)
    private String password;

    @ManyToMany(
            fetch = FetchType.LAZY,
            cascade = CascadeType.MERGE
    )
    @JoinTable(
            name = "user_role",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private List<Role> roles = Collections.emptyList();

    public User withId(final String id) {
        this.id = id;
        return this;
    }

    public User withAge(final int age) {
        this.age = age;
        return this;
    }

    public User withUsername(final String username) {
        this.username = username;
        return this;
    }

    public User withPassword(final String password) {
        this.password = password;
        return this;
    }

    public User withRoles(final Role... roles) {
        return withRoles(Arrays.stream(roles).collect(Collectors.toList()));
    }

    public User withRoles(final List<Role> roles) {
        this.roles = roles;
        return this;
    }
}

Role.java

@Data
@NoArgsConstructor
@Table
@Entity
public class Role implements Serializable {

    private static final long serialVersionUID = 812344454009121807L;

    @Id
    private String roleName;

    @ManyToMany(
            fetch = FetchType.LAZY,
            cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.DETACH }
    )
    @JoinTable(
            name = "role_resource_permission",
            joinColumns = @JoinColumn(name = "role_id"),
            inverseJoinColumns = @JoinColumn(name = "resource_permission_id")
    )
    private Set<ResourcePermission> resourcePermissions = Collections.emptySet();

    @ManyToMany(
            mappedBy = "roles",
            fetch = FetchType.LAZY,
            cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.DETACH }
    )
    private List<User> users = Collections.emptyList();


    public Role(final String roleName) {
        setRoleName(roleName);
    }

    public void setRoleName(final String roleName) {
        final RoleType roleType = RoleType.of(roleName);
        this.roleName = roleType.getRoleName();
        final Set<ResourcePermission> resourcePermissions = roleType.getResourcePermissions().stream()
                .map(ResourcePermissionType::getPermissionName)
                .map(ResourcePermission::new)
                .collect(Collectors.toSet());
        setResourcePermissions(resourcePermissions);
    }

    public void setResourcePermissions(final Set<ResourcePermission> resourcePermissions) {
        if (this.resourcePermissions.isEmpty()) {
            this.resourcePermissions = resourcePermissions;
        }
    }

}

ResourcePermission.java

@NoArgsConstructor
@Data
@Table
@Entity
public class ResourcePermission implements Serializable {

    private static final long serialVersionUID = 883231454000721867L;

    @Id
    private String permissionName;

    public ResourcePermission(final String permissionName) {
        setPermissionName(permissionName);
    }

    @ManyToMany(
            mappedBy = "resourcePermissions",
            fetch = FetchType.LAZY,
            cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.DETACH }
    )
    private Set<Role> roles = Collections.emptySet();

    public void setPermissionName(String permissionName) {
        final ResourcePermissionType permissionType = ResourcePermissionType.of(permissionName);
        this.permissionName = permissionType.getPermissionName();
    }
}

RoleType.java

@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum RoleType {

    DOCTOR("DOCTOR", doctorsPermissions()),
    TECHNICIAN("TECHNICIAN", technicianPermission()),
    ADMIN("ADMIN", adminPermissions());

    @Getter
    private String roleName;
    @Getter
    private final List<ResourcePermissionType> resourcePermissions;

     public static RoleType of(final String roleName) {
        return Arrays.stream(values())
                .filter(roleType -> roleType.getRoleName().equals(roleName.toUpperCase()))
                .findFirst()
                .orElseThrow(IllegalArgumentException::new);
     }

    private static List<ResourcePermissionType> doctorsPermissions() {
        return Arrays.asList(
                ENCOUNTER_READ, ENCOUNTER_WRITE,
                PATIENT_READ, PATIENT_WRITE
        );
    }

    private static List<ResourcePermissionType> adminPermissions() {
        return Arrays.asList(
                ENCOUNTER_READ, ENCOUNTER_WRITE,
                BUILDING_UNIT_READ, BUILDING_UNIT_WRITE,
                ORG_UNIT_READ, ORG_UNIT_WRITE
        );
    }

    private static List<ResourcePermissionType> technicianPermission() {
        return Arrays.asList(
                ENCOUNTER_READ, ENCOUNTER_WRITE,
                BUILDING_UNIT_READ, BUILDING_UNIT_WRITE
        );
    }

}

ResourcePermissoinType.java

@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum ResourcePermissionType implements Serializable {

    PATIENT_READ("PATIENT:READ"), PATIENT_WRITE("PATIENT:WRITE"),
    ENCOUNTER_READ("ENCOUNTER:READ"), ENCOUNTER_WRITE("ENCOUNTER:WRITE"),
    BUILDING_UNIT_READ("BUILDING_UNIT:READ"), BUILDING_UNIT_WRITE("BUILDING_UNIT:WRITE"),
    ORG_UNIT_READ("ORG_UNIT:READ"), ORG_UNIT_WRITE("ORG_UNIT:WRITE");

    @Getter
    private String permissionName;

    public static ResourcePermissionType of(final String permissionName) {
        return Arrays.stream(values())
                .filter(v -> v.getPermissionName().equals((permissionName.toUpperCase())))
                .findFirst()
                .orElseThrow(IllegalArgumentException::new);
    }

}

不幸的是,javax 持久性 API 不接受枚举作为实体。我也尝试过使用@Embeddable@IdClass,但这对我也不起作用。我无法生成我想到的模式。另一方面,使用该模型成功生成了模式。

目前,角色存储库和资源权限存储库都没有导出 (@RepositoryRestResource(..., exported = false)),因此为了让您保留这两个实体,您必须在 User 中提供该数据.请记住这一点,因为这也是我要讨论的讨论的一部分。

现在让我们检查一下UserCrudRepository 的集成测试,它将在成功验证后尝试添加新用户。

@TestMethodOrder(OrderAnnotation.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class UserCrudRepositoryApiITest {

private final List<User> testUsers = Arrays.asList(
                new User().withUsername("dummy_username_01").withPassword("dummy_password_01").withAge(35)
                        .withRoles(new Role("ADMIN")),
                new User().withUsername("dummy_username_02").withPassword("dummy_password_02").withAge(40)
                        .withRoles(new Role("DOCTOR")),
                new User().withUsername("dummy_username_03").withPassword("dummy_password_03").withAge(45)
        );
.
.
    @Order(1)
    @Test
    public void afterAuthenticationAddNewUser() throws Exception {
        final String generatedToken = login();
        // serialize the user
        final String requestJson = objectMapper.writeValueAsString(testUsers.get(0));
        final RequestBuilder request = MockMvcRequestBuilders.post(USER_CRUD_BASE_URL)
                .header(HttpHeaders.AUTHORIZATION, generatedToken)
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestJson);
        final String serializedContent = mvc.perform(request)
                .andExpect(status().isCreated())
                .andReturn()
                .getResponse()
                .getContentAsString();
        final User storedUser = objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .readValue(serializedContent, User.class);

        assertThat(storedUser).isEqualTo(testUsers.get(0));
    }
.
.
}

在这里,我得到一个状态码冲突 409,并且无法一次保存所有实体。

很遗憾,SO 只允许 30000 个字符,因此如果您想查看日志,请导航至 this repo

我的问题

  1. 我这辈子都无法理解参照完整性约束违规的位置 正在发生。有什么想法吗?
  2. 欢迎就如何以更好的方式对这些关系建模的任何建议!
  3. 我在使用 JPA 存储库时遇到的另一个问题是,保持角色和资源权限的唯一方法是在用户正文中提供该数据。我希望这些实体独立于用户进行管理(每个实体都有自己的单独存储库),所以我尝试导出他们的存储库。但是,问题在于您不再可以在User 的主体中传递Role 数据,而是对该实体的引用。有没有办法两全其美。

我希望我把我的问题说清楚了,如果没有,我很乐意详细说明。

【问题讨论】:

    标签: java spring hibernate jpa spring-data-jpa


    【解决方案1】:

    我猜当User 被持久化时,它也会为user_role 表执行插入操作,但该角色尚未持久化。您可以尝试先持久化 Role 或在 User#roles 关联处使用 PERSIST 级联。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-12-25
      • 1970-01-01
      • 2012-04-17
      • 2019-07-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多