JPA 1:N relationship removing child does not remove it from parent - java

I have the following objects:
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Entity(name="Group")
public class Group {
#Id
#GeneratedValue
#NotNull
#Column(name = "GROUP_ID")
private Long id;
#Column(name="NAME")
private String name;
#OneToMany(
targetEntity = Product.class,
mappedBy = "groupId",
cascade = CascadeType.ALL,
fetch = FetchType.EAGER,
orphanRemoval = true
)
private List<Product> products = new ArrayList<>();
public Group(String name) {
this.name = name;
}
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
#Entity(name="Product")
public class Product {
#Id
#GeneratedValue
#NotNull
#Column(name="PRODUCT_ID")
private Long id;
#Column(name="NAME")
private String name;
#Column(name="DESCRIPTION")
private String description;
#Column(name="PRICE")
private double price;
#ManyToMany
#JoinTable(
name = "JOIN_PRODUCT_CART",
joinColumns = {#JoinColumn(name = "PRODUCT_ID", referencedColumnName = "PRODUCT_ID")},
inverseJoinColumns = {#JoinColumn(name = "CART_ID", referencedColumnName = "CART_ID")}
)
private List<CartEntity> carts = new ArrayList<>();
#ManyToOne
#JoinColumn(name = "GROUP_ID")
private Group groupId;
public Product(String name, String description, double price) {
this.name = name;
this.description = description;
this.price = price;
}
public Product(String name, String description, double price, Group groupId) {
this(name, description, price);
this.groupId = groupId;
}
public void addToCart(CartEntity cart) {
this.carts.add(cart);
cart.getProductsList().add(this);
}
public void addGroup(Group group) {
group.getProducts().add(this);
this.groupId = group;
}
#Getter
#NoArgsConstructor
#AllArgsConstructor
#Entity(name = "cart")
public class CartEntity {
#Id
#NotNull
#GeneratedValue
#Column(name = "CART_ID")
private Long id;
#ManyToMany(cascade = CascadeType.ALL, mappedBy = "carts")
private List<Product> productsList = new ArrayList<>();
public void addProduct(Product product) {
productsList.add(product);
product.getCarts().add(this);
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CartEntity that = (CartEntity) o;
return id.equals(that.id);
}
#Override
public int hashCode() {
return Objects.hash(id);
}
}
Now, when I have the following test:
public class ProductDaoTestSuite {
#Autowired
private ProductDao productDao;
#Autowired
private CartDaoStub cartDaoStub;
#Autowired
private GroupDao groupDao;
#Test
public void testDeleteProduct() {
// Given
Product product = new Product("test", "testProduct", 100.0);
Group group = new Group("group1");
CartEntity cart = new CartEntity();
product.addGroup(group);
cart.addProduct(product);
// When
groupDao.save(group);
productDao.save(product);
cartDaoStub.save(cart);
Long groupId = group.getId();
Long productId = product.getId();
Long cartId = cart.getId();
productDao.deleteById(productId);
// Then
Assert.assertTrue(cartDaoStub.findById(cartId).isPresent());
Assert.assertEquals(0, cartDaoStub.findById(cartId).get().getProductsList().size());
Assert.assertTrue(groupDao.findById(groupId).isPresent());
Assert.assertEquals(0, groupDao.findById(groupId).get().getProducts().size());
Following product deletion, I would expect association with it in group and cart to disappear (product to disappear from their List relationship fields). However, that is not happening at the moment. When I use Group/Cart Dao to pull group & cart from the DB after product deletion, they still have product in their Lists, while product when pulled from the DB is returned as null.
I have tried to add "orphanRemoval = true" value for #OneToMany adnotation, but it did not seem to work for Group entity.
What am I doing wrong?
I have started experimenting with adding all types of cascade (except for REMOVE) to #ManyToOne on Product class, but so far no luck.

For 1:N, yours should work just fine with minor adjustment.
The reason why it fails: Upon doing "groupDao.save(group);" this group is now in the persistence context and calling "groupDao.findById(groupId).get().getProducts().size()" would return the copy which is from the persistence context.
To solve this: simply add: entityManager.flush(); and entityManager.clear(); before the Assert
I would like to demonstrate it with this Integration Test
#Test
#Transactional
public void deleteProduct_groupShouldNowBeEmpty() {
ProductGroup group = groupRepository.findById("0001").orElseThrow(() -> new IllegalArgumentException("id not found"));
Assert.assertEquals(1, group.getProducts().size());
Product product = productRepository.findById("0001").orElseThrow(() -> new IllegalArgumentException("id not found"));
productRepository.delete(product);
entityManager.flush();
entityManager.clear();
Assert.assertEquals(0, productRepository.findAll().size());
Assert.assertEquals(0, groupRepository.findById("0001").get().getProducts().size());
}
If we are to remove the first 2 lines, then we won't need to flush and clear. Like this.
#Test
#Transactional
public void deleteProduct_groupShouldNowBeEmpty() {
Product product = productRepository.findById("0001").orElseThrow(() -> new IllegalArgumentException("id not found"));
productRepository.delete(product);
Assert.assertEquals(0, productRepository.findAll().size());
Assert.assertEquals(0, groupRepository.findById("0001").get().getProducts().size());
}
For N:M, since there would be another table where product is being referenced, then we would need to delete the records from that table first before deleting the product.
N:M is a bit tricky so if I can suggest domain changes, here how I'll do it. (The integration test is at the bottom.)
I'll add a separate entity: CartItem
which is associated to a Product and Cart
#Entity
public class CartItem {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
#ManyToOne
private Product product;
#ManyToOne
private Cart cart;
public String getId() {
return id;
}
// Required by JPA
protected CartItem() {}
}
And for the Product Entity: add a bidirectional relationship with CartItem
#Entity
public class Product {
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
private String name;
private String description;
private BigDecimal price;
#ManyToOne
private ProductGroup group;
#OneToMany(mappedBy = "product")
private List<CartItem> cartItems;
public List<CartItem> getCartItems() {
return cartItems;
}
// Required by JPA
protected Product() {}
}
Then, retrieve the product (using Join Fetch to avoid N+1, since later will be looping through each cartItem)
public interface ProductRepository extends JpaRepository<Product, String> {
#Query("SELECT product FROM Product product JOIN FETCH product.cartItems")
Optional<Product> findProduct(String Id);
}
create another query inside CartItemRepository to delete cartItems in bulk by ids
public interface CartItemRepository extends JpaRepository<CartItem, String> {
#Modifying
#Query("DELETE FROM CartItem cartItem WHERE cartItem.id IN :ids")
void deleteByIds(#Param("ids") List<String> ids);
}
Lastly here's the integration test to wrap everthing up:
#Test
#Transactional
public void deleteProduct_associatedWithCart() {
Cart cart = cartRepository.findById("0001").get();
Assert.assertEquals(1, cart.getCartItems().size());
Product product = productRepository.findProduct("0001").orElseThrow(() -> new IllegalArgumentException("id not found"));
List<String> cartItemIds = product.getCartItems().stream()
.map(CartItem::getId)
.collect(Collectors.toList());
cartItemRepository.deleteByIds(cartItemIds);
productRepository.delete(product);
entityManager.flush();
entityManager.clear();
Assert.assertEquals(0, productRepository.findAll().size());
Assert.assertEquals(0, groupRepository.findById("0001").get().getProducts().size());
Assert.assertEquals(0, cartItemRepository.findAll().size());
Assert.assertEquals(0, cartRepository.findById("0001").get().getCartItems().size());
}
I've used DBUnit for this integration test so I think it would also be helpful to share the dataset.
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
<product_group id="0001" name="product group with 1 product"/>
<product id="0001" group_id="0001" />
<cart id="0001" />
<cart_item id="0001" product_id="0001" cart_id="0001" />
</dataset>

When you remove an entity, this state transition should be propagated from parent to child, not the other way around.
In this case, you need to move that functionally to the Group entity, something like this:
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Entity(name="Group")
public class Group {
#Id
#GeneratedValue
#NotNull
#Column(name = "GROUP_ID")
private Long id;
#Column(name="NAME")
private String name;
#OneToMany(
targetEntity = Product.class,
mappedBy = "groupId",
cascade = CascadeType.ALL,
fetch = FetchType.LAZY, // Always prefer LAZY initialized Collections to EAGER ones
orphanRemoval = true
)
private List<Product> products = new ArrayList<>();
public Group(String name) {
this.name = name;
}
public void addProduct(Product product){
product.setGroupId(this);
this.products.add(product);
}
public void removeProduct(Product product){
product.setGroupId(null);
this.products.remove(product);
}
If you want to remove a Product, you only need to invoke the removeProduct method and save the parent entity:
Group group = new Group("group1");
Product product = new Product("test", "testProduct", 100.0);
group.addProduct(product);
groupDao.save(group);
On the other hand, we have the many-to-many relation between Product and CartEntity.
First, if you configure the entity CartEntity with Cascade.ALL as in your example:
#ManyToMany(cascade = CascadeType.ALL, mappedBy = "carts")
private List<Product> productsList = new ArrayList<>();
It will have a probably undesired effect: if you remove the CartEntity, it will remove all the Products associated with the entity as well, even if other CartEntitys are still associated to them. Vlad Mihalcea explain it in great detail in this article.
To avoid that problem, the best option will be just define the relationship as follows:
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "carts")
private List<Product> productsList = new ArrayList<>();
This will give us a CartEntity like this:
#Getter
#NoArgsConstructor
#AllArgsConstructor
#Entity(name = "cart")
public class CartEntity {
#Id
#NotNull
#GeneratedValue
#Column(name = "CART_ID")
private Long id;
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "carts")
private List<Product> productsList = new ArrayList<>();
public void addProduct(Product product) {
productsList.add(product);
product.getCarts().add(this);
}
public void removeProduct(Product product) {
productsList.remove(product);
product.getCarts().remove(this);
}
public void removeProducts() {
for(Product product : new ArrayList<>(products)) {
removeProduct(product);
}
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CartEntity that = (CartEntity) o;
return id.equals(that.id);
}
#Override
public int hashCode() {
return Objects.hash(id);
}
}
Please, note the inclusion of the removeProduct and removeProducts methods.
With this code, if you need to remove a CartEntity, just do the following:
cart.removeProducts();
cartDao.remove(cart);
And if you need to remove a Product from the CartEntity (will only remove the relation):
cart.removeProduct(product);
cartDao.save(cart);
If you need to propagate the Product remove to the CartEntity, I think that the best option will be create a business method that takes care of the whole process. Think in something like:
public void removeProduct(Product product){
Group group = product.getGroupId();
group.removeProduct(product);
final List<CartEntity> carts = product.getCarts();
if (carts != null) {
for(CartEntity cart : new ArrayList<>(carts)) {
cart.removeProduct(product);
cartDao.save(cart);
}
}
groupDao.save(group);
}

It will remove the association, you just need to do small adjustments.
1:N. When you remove Product, you don't have to do anything else in order to remove its association with Group, because the product itself holds the association (in DB column product.group_id). You just need to commit the transaction. And next time when you load a group from the DB it for sure will not contain this product.
N:M. There is no way to automatically remove the association because it is stored in a separate table and you don't have a separate entity for it. (YOU SHOULD NOT USE CascadeType.ALL for N:M relations). What you want to do is remove the association before you remove the product. Just add another helper method to Product.
public void removeFromCarts() {
carts.forEach(c -> c.getProducts().remove(this));
carts.clear();
}
So finally, in order to remove a product and all the associations with it. You will need to do the following:
product.removeFromCarts();
productDao.deleteById(productId); // not sure why you remove by id (not pass object)
*please note that you need to commit transaction and close the session. So you cannot rely on the test. In real app when you do what I described, it will work
**N:M is tricky. For instance, you should better use Set instead of List to avoid unexpected SQL under the hood. Also going down the road, I recommend you to consider splitting N:M into two N:1 and 1:M and have a dedicated Entity for a link table

Not sure I follow. Hibernate does not automatically maintain the inverse association for you. You can make it sensitive to changes on the owning side of the association, but that's as far as it goes.
As to why your test fails, cartDaoStub.findById(cartId) probably returns the same copy of the CartEntity that you already have loaded into the persistence context. Try calling entityManager.flush() followed by entityManager.clear() before making the assertion and the issue will probably go away.

Related

How to retrieve data from parent-child tables using Spring Data JPA?

In my Spring Boot app, I use Hibernate and applied the necessary relations to the following entities properly.
#Entity
public class Recipe {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable=false, length=50)
private String title;
#OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL)
private List<RecipeIngredient> recipeIngredients = new ArrayList<>();
}
#Entity
public class RecipeIngredient {
#EmbeddedId
private RecipeIngredientId recipeIngredientId = new RecipeIngredientId();
#ManyToOne(optional = true, fetch = FetchType.LAZY)
#MapsId("recipeId")
#JoinColumn(name = "recipe_id", referencedColumnName = "id")
private Recipe recipe;
#ManyToOne(optional = true, fetch = FetchType.LAZY)
#MapsId("ingredientId")
#JoinColumn(name = "ingredient_id", referencedColumnName = "id")
private Ingredient ingredient;
}
#Entity
public class Ingredient
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(unique=true, nullable=false, length=50)
#EqualsAndHashCode.Include
private String name;
#OneToMany(mappedBy = "ingredient", cascade = CascadeType.ALL)
private Set<RecipeIngredient> recipeIngredients = new HashSet<>();
}
Now I am trying to retrieve data by merging related entities. For example, when retrieving a Recipe, I also need to retrieve all Ingredients belonging to this Recipe.
As far as I know, I can use Projection and maybe it is better to only use Hibernate features and retrieve related table data via Java Stream. I have no idea how should I retrieve data via Hibernate.
Suppose that I just need an Optional<Recipe> that has List<Ingredient>. Then, I probably need a DTO class something like that:
#Data
public class ResponseDTO {
private Long id;
private String title;
List<RecipeIngredient> ingredients;
// getter, setter, constructor
}
So, how should I populate this DTO with the requested Recipe and corresponding Ingredient data (getting Ingredient names besides id values) using Java Stream?
Or if you suggest Projection way, I tried it but the data is multiplied by the ingredient count belonging to the searched recipe.
Update:
#Getter
#Setter
#NoArgsConstructor
public class ResponseDTO {
private Long id;
private String title;
List<IngredientDTO> ingredientDTOList;
public ResponseDTO(Recipe recipe) {
this.id = recipe.getId();
this.title = recipe.getTitle();
this.ingredientDTOList = recipe.getRecipeIngredients().stream()
.map(ri -> new IngredientDTO(ri.getIngredient().getName()))
.toList();
}
}
#Getter
#Setter
public class IngredientDTO {
private Long id;
private String name;
public IngredientDTO(String name) {
this.name = name;
}
}
First, in the ResponseDTO you will need you change the type of ingredients from List<RecipeIngredient> to List<Ingredient>.
To manually perform the mapping, you should use (to map from a suppose Recipe recipe to a RespondeDTO response):
ResponseDTO recipeToResponseDTO(Recipe recipe) {
ResponseDTO response = new ResponseDTO();
response.setId(recipe.getId());
response.setTitle(recipe.getTitle());
response.setIngredients(recipe.recipeIngredients.stream()
.map(RecipeIngredient::getIngredient()
.collect(Collectors.toList());
return response;
}
On the other hand, to model a n-n relation, I encourage you to use the approach proposed by E-Riz in the comment.

SQl error - Referential integrity constraint violation when persisting several entities at once

Problem I'm trying to solve
I'm trying to model a #ManyToMany relation between a User and Role, such that a user can have n roles, and one role is referenced by several users. A role can be persisted even if it's not referenced by any user (detached), and a user with no roles is allowed too.
The same kind of relation must be built between Role and ResourcePermission.
To give you an idea about how each entity looks like:
Both ResourcePermission and Role have a finite set of values. For example, if Patient happens to be a resource, then one resource permission could be "PATIENT:READ" or "PATIENT:WRITE", and the role DOCTOR has several of these permissions. I hope it's clear sofar how my data model looks like.
What I'm using
Currently, I'm using spring-data-jpa version 2.4.2 to model my entities, and to create my CRUD repos. Except for base path and media type, I don't have any specific configuration (all is set to default).
Hibernate is my persistence provider atm .
Concerning my datasource, I'm using in-memory H2 for my development environment, and again no specific configuration there either.
How I'm solving it
Here's how my entities look like
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);
}
}
Unfortunately, the javax persistence API does not accept enums as entities. I tried using #Embeddable and #IdClass too, but that didn't work out for me either. I was not able to generate the schema that I had in mind. On the other hand, the schema was successfully generated using this model.
At the moment, both the Role repository as well as the Resource Permission repository are not exported (#RepositoryRestResource(..., exported = false)), so in order for you to persist those two entities, you'd have to provide that data in User. Keep that in mind, because that's also a part of the discussion that I want to talk about.
Now let's examine this integration test for the UserCrudRepository that will attempt to add a new user after a successful authentication.
#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));
}
.
.
}
In here, I'm getting a status code conflict 409, and not able to persist all entities at once.
Unfortunately, SO allows only 30000 character, so please navigate to this repo if you would like to take a look at the log.
My Questions
I couldn't for the life of me understand where that referential integrity constraint violation
is occurring. Any idea?
Any suggestions on how to model these relations in a better way are welcome!
Another problem I'm having with JPA repos is that the only way to persist roles and resource permissions is by providing that data in the user's body. I would like those entities to be managed independently of the user (each with its own separate repository), so I tried exporting their repositories. However, the problem then is that you no longer can pass Role data in the body of a User, but rather A reference to that entity. Is there a way to get the best of both worlds.
I hope I made my problem clear, if not, I'd be happy to elaborate more.
I guess when a User is persisted, it also does the insert for the user_role table, but the role wasn't persisted yet. You could try to persist the Role first or use PERSIST cascading at the User#roles association.

Spring Boot - Pagination for Many-to-Many Relationship with extra Column

I can't find a clean and simple way to do pagination when using a many-to-many relationship with an extra column.
My model looks like that:
I have a user and a product model. Each user can consume n products. Each consumption will be stored in an extra table, because I want to store extra information such as date etc. I have implemented the model as follows and it works, but I want to get the consumptions of an user as a Pageable rather than retrieving the whole set. What would be the best way to implement that ?
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToMany(
mappedBy = "user",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Consumption> consumptionList = new ArrayList<>(); // never set this attribute
public List<Consumption> getConsumptionList() {
return consumptionList;
}
public void addConsumption(Product product) {
Consumption consumption = new Consumption(this, product);
consumptionList.add(consumption);
product.getConsumptionList().add(consumption);
}
public void removeConsumption(Consumption consumption) {
consumption.getProduct().getConsumptionList().remove(consumption);
consumptionList.remove(consumption);
consumption.setUser(null);
consumption.setProduct(null);
}
}
--
#Entity
#NaturalIdCache
#org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToMany(
mappedBy = "product",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Consumption> consumptionList = new ArrayList<>();
public List<Consumption> getConsumptionList() {
return consumptionList;
}
}
This is my class to store consumptions.
#Entity
public class Consumption {
#EmbeddedId
private UserProductId id;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("userId")
private User user;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("productId")
private Product product;
public Consumption(User user, Product product) {
this.user = user;
this.product = product;
this.id = new UserProductId(user.getId(), product.getId());
}
}
And this is my composite Primary Key which.
#Embeddable
public class UserProductId implements Serializable {
#Column(name = "user_id")
private Long userId;
#Column(name = "product_id")
private Long productId;
private UserProductId() {
}
public UserProductId(Long userId, Long productId) {
this.userId = userId;
this.productId = productId;
}
}
I would like to be able to call a method such as "getConsumptionList(Page page)" which then returns a Pageable.
I hope you can help me!
Thank you in advance!
Well, if using Spring Boot you could use a Repository:
#Repository
public interface ConsumptionRepo extends JpaRepository<Consumption, Long>{
List<Consumption> findByUser(User user, Pageable pageable);
}
Then you can simply call it
ConsumptionRepo.findByUser(user, PageRequest.of(page, size);
I finally found a solution for my problem thanks to #mtshaikh idea:
Just implement a Paginationservice:
public Page<Consumption> getConsumptionListPaginated(Pageable pageable) {
int pageSize = pageable.getPageSize();
int currentPage = pageable.getPageNumber();
int startItem = currentPage * pageSize;
List<Consumption> list;
if (consumptionList.size() < startItem) {
list = Collections.emptyList();
} else {
int toIndex = Math.min(startItem + pageSize, consumptionList.size());
list = consumptionList.subList(startItem, toIndex);
}
return new PageImpl<>(list, PageRequest.of(currentPage, pageSize), consumptionList.size());
}

error refreshing JPA Entity

I have the following domain model
Currency ----< Price >---- Product
Or in English
A Product has one or more Prices. Each Price is denominated in a particular Currency.
Price has a composite primary key (represent by PricePK below) which is composed of the foreign keys to Currency and Product. The relevant sections of the JPA-annotated Java classes are below (getters and setters mostly omitted):
#Entity #Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Currency {
#Id
private Integer ix;
#Column
private String name;
#OneToMany(mappedBy = "pricePK.currency", cascade = CascadeType.ALL, orphanRemoval = true)
#LazyCollection(LazyCollectionOption.FALSE)
private Collection<Price> prices = new ArrayList<Price>();
}
#Entity #Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Product {
#Id #GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#OneToMany(mappedBy = "pricePK.product", cascade = CascadeType.ALL, orphanRemoval = true)
#LazyCollection(LazyCollectionOption.FALSE)
private Collection<Price> defaultPrices = new ArrayList<Price>();
}
#Embeddable
public class PricePK implements Serializable {
private Product product;
private Currency currency;
#ManyToOne(optional = false)
public Product getProduct() {
return product;
}
#ManyToOne(optional = false)
public Currency getCurrency() {
return currency;
}
}
#Entity #Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Price {
private PricePK pricePK = new PricePK();
private BigDecimal amount;
#Column(nullable = false)
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
#EmbeddedId
public PricePK getPricePK() {
return pricePK;
}
#Transient
public Product getProduct() {
return pricePK.getProduct();
}
public void setProduct(Product product) {
pricePK.setProduct(product);
}
#Transient
public Currency getCurrency() {
return pricePK.getCurrency();
}
public void setCurrency(Currency currency) {
pricePK.setCurrency(currency);
}
}
When I try to refresh an instance of Product, I get a StackOverflowError, so I suspect there's some kind of cycle (or other mistake) in the mapping above, can anyone spot it?
I've seen this error a few times, but I can't remember the exact solution. I have the idea that you need to remove mapping from PricePK (both #ManyToOne) and replace that with #AssociationOverrides on Price.
#Entity #Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
#AssociationOverrides({
#AssociationOverride(name = "pricePK.product",
joinColumns = #JoinColumn(name = "product_id")),
#AssociationOverride(name = "pricePK.currency",
joinColumns = #JoinColumn(name = "currency_id"))
})
public class Price extends VersionedEntity {
[...]
}
please check that the column names are ok, as I can't see the id columns on Product or Currency.
I encountered a similar problem and it turned out to be caused by the CascadeType.ALL defined in the OneToMany annotation. When you refresh product, it is going to try to refresh the prices that are in the persistent context.
Depending on the situation, you may be able to get by without having the prices refreshed when the product is refreshed in the entity manager, such as:
#OneToMany(mappedBy = "pricePK.product", cascade = {
CascadeType.PERSIST, CascadeType.MERGE
}, orphanRemoval = true)
#LazyCollection(LazyCollectionOption.FALSE)
private Collection<Price> defaultPrices = new ArrayList<Price>();
Shouldn't you just declare the relations from Price to Product and Currency as ManyToOne directly in the Price entity, and annotate Price with #IdClass(PricePK)?
I don't know how Hibernate handles this though, but I have successfully implemented this using OpenJPA. In that case, PricePK must declare its fields with the same names as in Price, but using the simple type instead (Integer instead of Currency or Product). In Price, you might need to annotate product and currency with #Id. Note that this is out of the JPA spec (#IdClass only supports simple fields).

JPA not saving foreign key to #OneToMany relation

I'm using Spring with Hibernate as a JPA provider and are trying to get a #OneToMany (a contact having many phonenumbers) to save the foreign key in the phone numbers table. From my form i get a Contact object that have a list of Phone(numbers) in it. The Contact get persisted properly (Hibernate fetches an PK from the specified sequence). The list of Phone(numbers) also gets persisted with a correct PK, but there's no FK to the Contacts table.
public class Contact implements Serializable {
#OneToMany(mappedBy = "contactId", cascade = CascadeType.ALL, fetch=FetchType.EAGER)
private List<Phone> phoneList;
}
public class Phone implements Serializable {
#JoinColumn(name = "contact_id", referencedColumnName = "contact_id")
#ManyToOne
private Contact contactId;
}
#Repository("contactDao")
#Transactional(readOnly = true)
public class ContactDaoImpl implements ContactDao {
#Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
public void save(Contact c) {
em.persist(c);
em.flush();
}
}
#Controller
public class ContactController {
#RequestMapping(value = "/contact/new", method = RequestMethod.POST)
public ModelAndView newContact(Contact c) {
ModelAndView mv = new ModelAndView("contactForm");
contactDao.save(c);
mv.addObject("contact", c);
return mv;
}
}
Hopefully I got all of the relevant bits above, otherwise please let me know.
You have to manage the Java relationships yourself. For this kind of thing you need something like:
#Entity
public class Contact {
#Id
private Long id;
#OneToMany(cascade = CascadeType.PERSIST, mappedBy = "contact")
private List<Phone> phoneNumbers;
public void addPhone(PhoneNumber phone) {
if (phone != null) {
if (phoneNumbers == null) {
phoneNumbers = new ArrayList<Phone>();
}
phoneNumbers.add(phone);
phone.setContact(this);
}
}
...
}
#Entity
public class Phone {
#Id
private Long id;
#ManyToOne
private Contact contact;
...
}
In reply to Cletus' answer. I would say that it's important to have the #column annotation on the id fields, as well as all the sequence stuff. An alternative to using the mappedBy parameter of the #OneToMany annotation is to use the #JoinColumn annotation.
As a kinda aside your implementation of addPhone needs looking at. It should probably be something like.
public void addPhone(PhoneNumber phone) {
if (phone == null) {
return;
} else {
if (phoneNumbers == null) {
phoneNumbers = new ArrayList<Phone>();
}
phoneNumbers.add(phone);
phone.setContact(this);
}
}
If the Contact-Phone relationship is unidirectional, you can also replace mappedBy in #OneToMany annotation with #JoinColumn(name = "contact_id").
#Entity
public class Contact {
#Id
private Long id;
#OneToMany(cascade = CascadeType.PERSIST)
#JoinColumn(name = "contact_id")
private List<Phone> phoneNumbers;
// normal getter/setter
...
}
#Entity
public class PhoneNumber {
#Id
private Long id;
...
}
Similar in JPA #OneToMany -> Parent - Child Reference (Foreign Key)
I don't think the addPhone method is necessary, you only have to set the contact in the phone object:
phone.setContact(contact);
If you want your relationship unidirectional i.e. can navigate from Contact to Phone's only, you need to add
#JoinColumn(name = "contact_id", nullable = false)
Under your #OneToMany on your parent entity.
nullable = false IS VITAL if you want hibernate to populate the fk on the child table
Try this sample:
#Entity
public class Contact {
#Id
private Long id;
#JoinColumn(name = "contactId")
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Phone> phones;
}
#Entity
public class Phone {
#Id
private Long id;
private Long contactId;
}
In JPA this helped me
contact.getPhoneList().forEach(pl -> pl.setContact(contact));
contactRepository.save(contact);

Categories