Search code examples
springspring-bootspring-data-jpafasterxml

spring boot - how to save many-to-one entities to database


In this spring boot app I have a one-to-many mapping from Cart to Item where the serialization cycle is solved by ignoring the Cart in Item's serialization. (So the Cart field in Item carries the @JsonBackReference annocation)

Spring boot initializes the database successfully such that GET-ing all Carts gives the json

[
    {
        "id": 1, <-- cart 1
        "items": [
            {
                "id": 0, <-- item 0
                "itemName": "tooth brush"
            },
            {
                "id": 1,
                "itemName": "shampoo"
            }
        ]
    },
    {
        "id": 2,
        "items": [
            {
                "id": 2,
                "itemName": "body wash"
            }
        ]
    },
    {
        "id": 3,
        "items": []
    }
]

However when I want to add an Item to an existing Cart the new Item is saved as null

@PostMapping("carts/{cartId}")
    public Cart addItemToCart(@PathVariable("cartId") long cartId, @RequestBody Item item)
    {
        LOG.info("Trying to add ITEM: id={}, name={}, cart={} to CART: {}", item.getId(), item.getItemName(), item.getCart(), cartId);

        Cart cart = dao.getById(cartId);
        LOG.info("I) CART: id={}, items={}", cart.getId(),cart.getItems().stream().map(i -> i.getItemName()).collect(Collectors.toList()));

        // also tried at this location: item.setCart(cart);
        cart.getItems().add(item);
        LOG.info("II) CART: id={}, items={}", cart.getId(),cart.getItems().stream().map(i -> i.getItemName()).collect(Collectors.toList()));

        Cart res = dao.save(cart); // HERE CART SAVES NEW ITEM AS NULL
        LOG.info("III) CART: id={}, items={}", res.getId(),res.getItems().stream().map(i -> i.getItemName()).collect(Collectors.toList()));

        Cart resCheck = dao.getById(res.getId());
        LOG.info("IV) CART: id={}, items={}", resCheck.getId(),resCheck.getItems().stream().map(i -> i.getItemName()).collect(Collectors.toList()));
        return res;
    }

Say I want to add an Item with name "beer" to Cart with cartid=1. In my opinion, I only need the itemName in the json because the Cart field is ignored (@JsonBackReference) and the itemid is autogenerated.

So, posting

{
    "itemName":"beer"
}

localhost:9200/demo/api/carts/1 gives the log (compare with above code)

Trying to add ITEM: id=0, name=beer, cart=null to CART: 1
I) CART: id=1, items=[tooth brush, shampoo]
II) CART: id=1, items=[tooth brush, beer, shampoo]
III) CART: id=1, items=[null, tooth brush, shampoo] <-- beer itemName is null after dao.save()
IV) CART: id=1, items=[null, tooth brush, shampoo]

And that's unsurprising because Carts and Items are joined on cartid but where should the beer Item get a cartid from when serializing an Item ignores any Cart info! What also doesnt work if I POST

{
    "itemName":"beer",
    "cart":{
        "id":1,
        "items":[]
    }
}

which also saves new item as null.

What I really want to avoid is switching JsonBackReference and JsonManagedReference.

How can I still add Items to Carts?

Here are the entity classes

@Entity
@Table(name="items")
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public class Item
{
    @Id
    @GenericGenerator(name="itemIdGen" , strategy="increment")
    @GeneratedValue(generator="itemIdGen")
    @Column(name = "itemid", nullable=false)
    private long id;

    @Column(name="itemname")
    private String itemName;

    @ManyToOne
    @JoinColumn(name = "cartid", nullable=false)
    @JsonBackReference
    private Cart cart;
    /*...*/
}

@Entity
@Table(name="carts")
public class Cart 
{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "cartid", nullable=false)
    private long id;

    @Column(name="items")
    @OneToMany(mappedBy = "cart")
    @JsonManagedReference
    private Set<Item> items;
    /*..*/
}

Thanks for the help

P.S.: If you want to clone the repo linked in the beginning you also need to run the eureka server and clone the demo-commons project.


Solution

  • The solution is to forget about referencing the entire Cart object in Item but just keep the cartid (on which is merged)

    @Entity
    @Table(name="carts")
    public class Cart 
    {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        @Column(name = "cartid", nullable=false)
        private long id;
    
    //  @Column(name="items")
    //    @OneToMany(mappedBy = "cart")
    //  @JsonManagedReference
    //    private Set<Item> items;
        @OneToMany(cascade = CascadeType.ALL)
        @JoinColumn(name="cartid", referencedColumnName="cartid")
        private Set<Item> items;
        /*..*/
    }
    
    @Entity
    @Table(name="items")
    @Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
    public class Item
    {
        @Id
        @GenericGenerator(name="itemIdGen" , strategy="increment")
        @GeneratedValue(generator="itemIdGen")
        @Column(name = "itemid", nullable=false)
        private long id;
    
        @Column(name="itemname")
        private String itemName;
    
    //    @ManyToOne
    //    @JoinColumn(name = "cartid", nullable=false)
    //    @JsonBackReference
    //    private Cart cart;
        @Column(name="cartid")
        private Long cartId;
        /*..*/
     }
    

    Now I can just happily POST the Item

    {
        "itemName":"beer",
        "cartid":1
    }