情境
在 JPA 中處理 多對多 (Many-to-Many) 關係, 不使用 @ManyToMany 註解方式, 而是將這個關係拆解為兩個一對多(One-to-Many)的單向關係,並為中間表創建一個獨立的Entity.
代碼如下:
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "first_name", nullable = false)
private String firstName;
@Column(name = "last_name")
private String lastName;
@Column(name = "email", nullable = false, unique = true)
private String email;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<UserRole> roles = new HashSet<>();
public User(String username, String password, String firstName, String lastName, String email) {
this.username = username;
this.password = password;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
}
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 20)
private String name;
public Role(String name) {
this.name = name;
}
@OneToMany(mappedBy = "role", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<UserRole> userRoles = new HashSet<>();
}
@Data
@Embeddable
public class UserRoleId implements Serializable {
// 與 UserRole.java 中 @MapsId 的名稱一致
@Column(name = "user_id")
private Long userId;
@Column(name = "role_id")
private Long roleId;
public UserRoleId() {
}
public UserRoleId(Long userId, Long roleId) {
this.userId = userId;
this.roleId = roleId;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
UserRoleId that = (UserRoleId) o;
return Objects.equals(userId, that.userId) && Objects.equals(roleId, that.roleId);
}
@Override
public int hashCode() {
return Objects.hash(userId, roleId);
}
}
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "users_roles")
public class UserRole implements Serializable {
// 複合主鍵的 ID
@EmbeddedId
private UserRoleId id;
// ManyToOne 關係到 User, userId 對映到 UserRoleId 中的 userId
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("userId")
@JoinColumn(name = "user_id")
private User user;
// ManyToOne 關係到 Role
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("roleId")
@JoinColumn(name = "role_id")
private Role role;
@Column(name = "assigned_at")
private LocalDateTime assignedAt;
}
當我們 序列化 User 實例時,Jackson會拋出JsonMappingException異常
顯示 Exception 如下:
這個錯誤發生在 Jackson 嘗試將您的 JPA 實體 User 序列化為 JSON 字串時,Jackson 序列化器仍然發現了一個循環
無限遞歸序列化 (Infinite Recursion) 錯誤原因:
循環序列化的多對多結構 User <-> UserRole <-> Role
- Jackson 序列化 User。
- 在序列化 User 的屬性時,遇到 roles 集合 (Set<UserRole>)。
- 序列化 UserRole 時,遇到 User 實體 (@ManyToOne private User user;)。
- Jackson 再次嘗試序列化這個 User 物件,回到步驟 1,形成無限循環。
‘’’
- Jackson 序列化 User。
- 在序列化 User 的屬性時,遇到 roles 集合 (Set<UserRole>)。
- 序列化 UserRole 實體。
- 在序列化 UserRole 時,遇到 User 實體 (@ManyToOne private User user;)。
- Jackson 再次嘗試序列化這個 User 物件,回到步驟 1,形成無限循環。
‘’’
註: Jackson 預設的最大遞歸深度是 1000 層,當達到這個限制時,它會拋出這個錯誤以避免堆棧溢出(StackOverflowError)。
任務
針對 User 序列化為 JSON 字串時,Jackson JSON 的無限遞迴問題,探討處理雙向關係的方法。
處理動作
步驟一. 首先建立一個測試案例測試:
@Transactional // 確保每個測試都回滾
@SpringBootTest
public class UserRoleRelationshipTest {
@Test
void testReadUserRoleRelationship() {
try {
List<User> users = userRepository.findAll(); // 獲取所有用戶
System.out.println("****** 獲取所有用戶: ******");
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
String jsonArray = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(users);
System.out.println(jsonArray);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
步驟二. 預備測試資料,已存DB
測試用
Table users
Table roles
Table users_roles
步驟三. 實作方案
使用@JsonIgnore
不想序列化某個屬性,使用 @JsonIgnore 註解來忽略關係中的某個屬性
選項1
public class User {
. . .
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
private Set<UserRole> roles = new HashSet<>();
. . .
}
測試結果
% mvn test
選項2
public class UserRole implements Serializable {
. . .
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("userId") // 映射到 UserRoleId 中的 userId 屬性
@JoinColumn(name = "user_id")
@JsonIgnore
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("roleId") // 映射到 UserRoleId 中的 roleId 屬性
@JoinColumn(name = "role_id")
@JsonIgnore
private Role role;
. . .
}
測試結果
% mvn test
使用@JsonManagedReferences和@JsonBackReferences
@JsonManagedReferences 用於引用父 Object ,@JsonBackReferences 用於標記子 Object。
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private Set<UserRole> roles = new HashSet<>();
@OneToMany(mappedBy = "role", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private Set<UserRole> userRoles = new HashSet<>();
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("userId") // 映射到 UserRoleId 中的 userId 屬性
@JoinColumn(name = "user_id")
@JsonBackReference
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("roleId") // 映射到 UserRoleId 中的 roleId 屬性
@JoinColumn(name = "role_id")
@JsonBackReference
private Role role;
執行結果:
留言