0

I'm currently working on a Spring Boot project for an online shop. It's my first project with Spring Boot (and my first post here), so my coding is not the best.

Context for the questions:

My shop (for now) has a lists of products and whishlists of different users (shopping lists), which have a bidirectional @ManyToMany relation (i left here the relevant details for my question(s)):

Product.java entity:

@Entity
public class Product extends RepresentationModel\<Product\>{
    @Id
    @GeneratedValue
    @JsonView(ProductView.DescriptionExcluded.class)
    private Integer id;
    
    @ManyToMany()
        @JoinTable(
            name = "Shopping_Product", 
            joinColumns = { @JoinColumn(name = "id", referencedColumnName = "id") }, 
            inverseJoinColumns = { @JoinColumn(name = "list_id", referencedColumnName = "list_id") })
    @JsonIgnore
    private Set<ShoppingList> shoppinglists = new HashSet<>();

// Constructor, getters, setters ....

ShoppingList.java entity:

@Entity public class ShoppingList {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @JsonView(ShoppingListView.ProductsExcluded.class)
    private Integer list_id;
    
    @JsonView(ShoppingListView.ProductsIncluded.class)
    @ManyToMany(mappedBy = "shoppinglists")
    private Set<Product> products = new HashSet<>();
   
// Constructor, getters, setters ...

I chose Product as the owner because i wanted to delete (tho it would be more fit to show something like "offer expired", but I'll stick to delete for now) the product from all existing lists when the admin takes it down from the shop, which works as expected:

ProductResource.java (controller):


    @DeleteMapping("/categs/*/sub/*/products/{id}")
    public ResponseEntity<String> deleteProduct(@PathVariable int id) {
        Optional<Product> optional = productRepository.findById(id);
        if(!optional.isPresent()) throw new NotFoundException("Product id - " + id);
        Product prod = optional.get();
        productRepository.delete(prod);
        return ResponseEntity.ok().body("Product deleted");
    }

My problems now are related to the ShoppingList entity, which is not the owner. Any call I make to the Product resource (controller) works as expected, but anything from the other side either fails or returns incomplete results, like the following:

1.

I call retrieve all products from a list and it returns only the first object (the list has at least 2): ShoppingListResource.java (controller):

@RestController
public class ShoppingListResource {

    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private ShoppingListRepository shoppinglistRepository;

    @GetMapping("/user/lists/{id}")
    public Set<Product> getShoppinglistProducts(@PathVariable int id) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String currentPrincipalName = authentication.getName();
        
        ShoppingList shoppingList = shoppinglistRepository.findById(id).get();
        String name = shoppingList.getUser().getUsername();
        if(!Objects.equals(currentPrincipalName, name)) throw new IllegalOperation("You can only check your list(s)!");

//      All lists are shown for a product
//      Product p = productRepository.findById(10111).get();
//      Set<ShoppingList> set = p.getShoppinglists();
//      set.stream().forEach(e -> log.info(e.toString()));
        
//      Only first product is shown for a list
        return shoppingList.getProducts();

This is what hibernate does on the last row (only returns 1/2 products)

Hibernate: select products0_.list_id as list_id2_3_0_, 
products0_.id as id1_3_0_, 
product1_.id as id1_1_1_, 
product1_.description as descript2_1_1_, 
product1_.name as name3_1_1_, 
product1_.price as price4_1_1_, 
product1_.subcat_id as subcat_i5_1_1_ from shopping_product products0_ inner join product product1_ on products0_.id=product1_.id where products0_.list_id=?

As i said above, I can delete a product and it gets removed automatically from all existing lists, but when i try the same from ShoppingList entity does nothing:

Same controller

    @DeleteMapping("/user/lists/{id}")
    public ResponseEntity<String> deleteShoppinglist(@PathVariable int id) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String currentPrincipalName = authentication.getName();
        
        ShoppingList shoppingList = shoppinglistRepository.findById(id).get();
        String name = shoppingList.getUser().getUsername();
        if(!Objects.equals(currentPrincipalName, name)) throw new IllegalOperation("You can only delete your list(s)!");
        
        shoppinglistRepository.delete(shoppingList);    
        
        return ResponseEntity.ok().body("Shopping list deleted");
    }

Also, when i try to add/delete product from an existing list, does nothing. This is my repo with full code, if you'd like to test directly (dev branch is up to date): https://github.com/dragostreltov/online-store/tree/dev

You can just use admin admin as authentication (on the H2 console too). More details on the readme. All DB data at app start is inserted from a .sql file.

I checked other similar questions and tried different methods on my ShoppingList entity (on the delete issue), like:

@PreRemove
public void removeListsFromProducts() {
    for(Product p : products) {
        p.getShoppinglists().remove(this);
    }
}

Spring/Hibernate: associating from the non-owner side

And still doesn't work.

UPDATE:

I found out what issues I was having, I'll post an answer with the solution.

DragosS
  • 1
  • 2

1 Answers1

0

For anyone who's got the same/similar problems as I did, this is how I resolved them:

For point 1

(Hibernate only retrieves the first product from a shoppingList (Set))

I made multiple tests on my retrieve method and found out my Set was only containing 1 object, despite calling .add(product) twice.

As you can see, I'm using HashSet for both entities:

In Product (owner):

private Set<ShoppingList> shoppinglists = new HashSet<>();

In ShoppingList (mappedBy):

private Set<Product> products = new HashSet<>();

Thanks to this answer: https://stackoverflow.com/a/16344031/18646899 I learnt:

HashSet (entirely reasonably) assumes reflexivity, and doesn't check for equality when it finds that the exact same object is already in the set, as an optimization. Therefore it will not even call your equals method - it considers that the object is already in the set, so doesn't add a second copy.

In particular, if x.equals(x) is false, then any containment check would also be useless.

Taking this into account, I overwrote the hashCode() and equals() methods in Product.class and now

shoppingList.getProducts()

works as expected.

For point 2

(not being able to delete associations of non-owner entity before deleting the row from it's table)

Added lazy fetch and cascade to Product @ManyToMany:

@ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH})

And added the following methods:

In Product class:

    public void addShoppinglist(ShoppingList list) {
        this.shoppinglists.add(list);
        list.getProducts().add(this);
    }
    
    public void removeShoppinglist(ShoppingList list) {
        this.shoppinglists.remove(list);
        list.getProducts().remove(this);
    }

In ShoppingList class:

   public void addProduct(Product product) {
        this.products.add(product);
        product.getShoppinglists().add(this);
    }
    
    public void removeProduct(Product product) {
        this.products.remove(product);
        product.getShoppinglists().remove(this);
    }

Added @Transactional and modified the method inside the controller (ShoppingListResource) for deleteShoppingList:

@RestController
public class ShoppingListResource {

...

    @Transactional
    @DeleteMapping("/user/lists/{id}")
    public ResponseEntity<String> deleteShoppinglist(@PathVariable int id) {
        ...

        shoppingList.getProducts().stream().forEach(e -> {
            e.removeShoppinglist(shoppingList);
        });
        shoppinglistRepository.delete(shoppingList);
        
        return ResponseEntity.ok().body("Shopping list deleted");
    }
}

And now this is working as expected, the shoppingList's associations are deleted first then the shoppingList itself.

DragosS
  • 1
  • 2