问题描述
我正在尝试解决的问题
我正在尝试对 @ManyToMany
和 User
之间的 Role
关系建模,以便用户可以拥有 n 个角色,其中一个角色是被多个用户引用。一个角色可以被持久化,即使它没有被任何用户引用(分离),也允许一个没有角色的用户。
必须在 Role
和 ResourcePermission
之间建立相同类型的关系。
让您了解每个实体的外观:
-
ResourcePermission
和Role
都有一组有限的值。例如,如果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,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",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,BUILDING_UNIT_READ,BUILDING_UNIT_WRITE,ORG_UNIT_READ,ORG_UNIT_WRITE
);
}
private static List<ResourcePermissionType> technicianPermission() {
return Arrays.asList(
ENCOUNTER_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)
),因此为了持久化这两个实体,您必须在 { 中提供该数据{1}}。请记住这一点,因为这也是我想谈论的讨论的一部分。
现在让我们检查 User
的集成测试,该测试将在成功验证后尝试添加新用户。
UserCrudRepository
在这里,我收到状态代码冲突 409,并且无法一次保留所有实体。
很遗憾,SO 只允许 30000 个字符,因此如果您想查看日志,请导航到 this repo。
我的问题
- 我一生都无法理解违反参照完整性约束的地方 正在发生。有什么想法吗?
- 欢迎就如何更好地建模这些关系提出任何建议!
- 我在使用 JPA 存储库时遇到的另一个问题是,保留角色和资源权限的唯一方法是在用户正文中提供该数据。我希望独立于用户管理这些实体(每个实体都有自己单独的存储库),因此我尝试导出它们的存储库。但是,问题是您不能再在
@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)); } . . }
的主体中传递Role
数据,而是传递对该实体的引用。有没有办法两全其美。
我希望我说清楚了我的问题,如果没有,我很乐意详细说明。
解决方法
我猜当 User
被持久化时,它也会对 user_role
表进行插入,但角色还没有被持久化。您可以尝试先保留 Role
或在 User#roles
关联中使用 PERSIST 级联。