SaitamaCoding
SaitamaCoding

Reputation: 33

Hibernate saveOrUpdate-method creates new entry/row instead of updating the existing one

I'm currently making a website to provide product management for a registered user. It uses spring + hibernate + mysql + jsp. Hibernates saveOrUpdate-method creates always a new entry/row to the database for the ProductDetail-entity instead of updating the already existing one. I have done the mapping according to the tutorials out there and I cannot understand what makes it to create a new row, because I have established the relationships to the Product- and ProductDetail-entity (OneToOne) in the Controller layer before the use. Someone save me from this struggle, I have been stuck here for more than 3 months... Below I will provide pictures of the Entities, Controller and DAO.

Product Entity (nvm commented annotations)

@Entity
@Table(name = "product")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @NotNull(message = "Product name is required")
    @Size(min = 1, message = "is required")
    @Column(name = "product_name")
    private String productName;

    @DecimalMin(value = "0.01", inclusive = true, message = "Price must be a minimum of 0.01$")
    @Digits(integer = 6, fraction = 2, message = "Price out of bounds, limit <6 digits>.<2 digits>")
    @Column(name = "price")
    private float price;

    @NotNull(message = "Quantity is required")
    @Min(value = 1, message = "Must be greater than zero")
    @Column(name = "qty")
    private Integer quantity;

    @NotNull(message = "Email is required")
    @Email(message = "Provide a valid email address")
    @Pattern(regexp = ".+@.+\\..+", message = "Provide a valid email address")
    @Column(name = "added_by")
    private String addedBy;

    @Column(name = "creation_datetime")
    private Date createDateTime;


    //@Version
    @Column(name = "last_updated")
    private Date updateDateTime;


    //@Valid
    @OneToOne(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private ProductDetail productDetail;

    @Column(name = "value_in_stock")
    private float valueInStock;

    public Product() {
        this.createDateTime = new Date();
        this.updateDateTime = this.createDateTime;
    }

    public Product(String productName, float price, Integer quantity, String addedBy) {
        this.productName = productName;
        this.price = price;
        this.quantity = quantity;
        this.addedBy = addedBy;
        this.valueInStock = this.price * this.quantity;
    }

        public void setProductDetail(ProductDetail productDetail) {
        if (productDetail == null) {
            if (this.productDetail != null) {
                this.productDetail.setProduct(null);
            }
        } else {
            productDetail.setProduct(this);
        }
        this.productDetail = productDetail;
    }
// getters and setters

ProductDetail entity

@Entity
@Table(name = "product_detail")
public class ProductDetail {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @NotNull(message = "A descriptionis required")
    @Column(name = "description")
    private String description;

    @NotNull(message = "Category is required")
    @Column(name = "category")
    private String category;

    @DecimalMin(value = "0.001", inclusive = true, message = "Must a minimum of 0.001g")
    @Digits(integer = 7, fraction = 2, message = "Weight out of bounds, limit <7 digits>.<2 digits>")
    @Column(name = "weight_g")
    private float weight;

    @NotNull(message = "Manufacturer is required")
    @Column(name = "manufacturer")
    private String manufacturer;

    @NotNull(message = "Provide a country")
    @Column(name = "made_in_country")
    private String countryMadeIn;

    @OneToOne(fetch=FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "product_id")
    private Product product;

    public ProductDetail() {

    }

    public ProductDetail(String description, String category, String manufacturer, String madeIn) {
        this.description = description;
        this.category = category;
        this.manufacturer = manufacturer;
        this.countryMadeIn = madeIn;
    }
// Getters and setters...

ProductController

Method to fetch form for adding a new product (GET request)

@GetMapping("/add")
public String getAddForm(Model model) {

        // create model attribute to bind all form data
        Product product = new Product();
        ProductDetail productDetail = new ProductDetail();

        // associating product and product details
        product.setProductDetail(productDetail);
        product.getProductDetail());

        model.addAttribute("categoryMap", categoryOptions);
        model.addAttribute("countryMap", countryOptions);
        model.addAttribute("product", product);

        return "product-form";
    }

Method to fetch product by id from productService (delegates data fetching to productDAO) (GET request)

@GetMapping("/updateProduct")
public String getUpdateForm(@RequestParam("productId") int productId, Model model) {

        // get product from db
        Product product = productService.getProduct(productId);
        product.getProductDetail());

        // set product as a model to pre-populate the form
        model.addAttribute("categoryMap", categoryOptions);
        model.addAttribute("countryMap", countryOptions);
        model.addAttribute("product", product);

        return "product-form";
    }

Method to process saving/updating the Product and its ProductDetail (POST request)

@PostMapping("/save")
public String saveOrUpdate(@Valid @ModelAttribute("product") Product product, BindingResult bindingResult,
            Model model) {

        // if result set has errors, return to product form with errors
        if (bindingResult.hasErrors()) {
            model.addAttribute("categoryMap", categoryOptions);
            model.addAttribute("countryMap", countryOptions);
            return "product-form";
        } else {

            // calculate value in stock to product before saving
            product.setValueInStock();

            productService.saveProduct(product);

            return "redirect:/";
        }

    }

ProductDAOImpl

Method to saveOrUpdate given product

@Override
    public void saveProduct(Product product) {

        // get current hibernate session
        Session session = sessionFactory.getCurrentSession();

        // save or update given product
        session.saveOrUpdate(product);
    }

Method to fetch product by its id

@Override
    public Product getProduct(int id) {

        // get current hibernate session
        Session session = sessionFactory.getCurrentSession();

        Query<Product> query = 
                session.createQuery("SELECT p FROM Product p "
                        + "JOIN FETCH p.productDetail "
                        + "WHERE p.id=:productId",
                        Product.class);

        // set parameters in query
        query.setParameter("productId", id);

        // execute and get product
        Product product = query.getSingleResult();

        return product;
    }

And finally here is the JSP form itself

<form:form action="save" modelAttribute="product" method="POST">

                <!-- associate data with product id -->
                <form:hidden path="id" />

                <div class="form-group row">
                    <label for="nameInput" class="col-sm-2 col-form-label">Product name *:</label>
                    <div class="col-sm-10">
                        <form:input path="productName" cssClass="form-control" id="nameInput" placeholder="Enter name" />
                        <form:errors path="productName" cssClass="errors" />
                    </div>
                </div>

                <div class="form-group row">
                    <label for="priceInput" class="col-sm-2 col-form-label">Price($) *:</label>
                    <div class="col-sm-10">
                        <form:input path="price" cssClass="form-control" id="priceInput" placeholder="Enter price" />
                        <form:errors path="price" cssClass="errors" />
                    </div>
                </div>

                <div class="form-group row">
                    <label for="quantityInput" class="col-sm-2 col-form-label">Quantity *:</label>
                    <div class="col-sm-10">
                        <form:input path="quantity" cssClass="form-control" id="quantityInput" placeholder="Enter qty" />
                        <form:errors path="quantity" cssClass="errors" />
                    </div>
                </div>


                <div class="form-group row">
                    <label for="emailInput" class="col-sm-2 col-form-label">Added by(email) *:</label>
                    <div class="col-sm-10">
                        <form:input path="addedBy" cssClass="form-control" id="emailInput" placeholder="[email protected]" />
                        <form:errors path="addedBy" cssClass="errors"  />
                    </div>
                </div>

                <div id="separator" > </div>


                <h5 id="header" >Product Details (Can be updated later)</h5>

                <div class="form-group row">
                    <label for="categoryInput" class="col-sm-2 col-form-label">Category *:</label>
                    <div class="col-sm-3">
                        <form:select path="productDetail.category" id="categoryInput" cssClass="form-control">
                            <form:option value="" label="Select Product Category" />
                            <form:options items="${categoryMap}"/>
                        </form:select>
                        <form:errors path="productDetail.category" cssClass="errors"  />
                    </div>
                </div>

                <div class="form-group row">
                    <label for="manufacturerInput" class="col-sm-2 col-form-label">Manufacturer *:</label>
                    <div class="col-sm-4">
                        <form:input path="productDetail.manufacturer" cssClass="form-control" id="manufacturerInput" placeholder="Enter manufacturer" />
                        <form:errors path="productDetail.manufacturer" cssClass="errors" />
                    </div>

                    <label for="madeInInput" class="col-sm-2 col-form-label">Country *:</label>
                    <div class="col-sm-3">
                        <form:select path="productDetail.countryMadeIn" id="madeInInput" cssClass="form-control">
                            <form:option value="" label="Country manufactured in" />
                            <form:options items="${countryMap}"/>
                        </form:select>
                        <form:errors path="productDetail.countryMadeIn" cssClass="errors"  />
                    </div>
                </div>


                <div class="form-group row">
                    <label for="descriptionInput" class="col-sm-2 col-form-label">Description *:</label>
                    <div class="col-sm-10">
                        <form:textarea path="productDetail.description" cssClass="form-control" id="descriptionInput" placeholder="Short description of product..." />
                        <form:errors path="productDetail.description" cssClass="errors"  />
                    </div>
                </div>


                <div class="form-group row">
                    <label for="weightInput" class="col-sm-2 col-form-label">Weight(g):</label>
                    <div class="col-sm-10">
                        <form:input path="productDetail.weight" cssClass="form-control" id="weightInput" placeholder="Enter weight" />
                    </div>
                </div>

                <input type="submit" value="Add" class="btn btn-primary" />

</form:form>

So the user should be able to add and update a product. At the moment this just adds and when the user wants to update the product, it just creates a new ProductDetail-entity instead of updating the ProductDetail on the fetched Product-entity.

Upvotes: 0

Views: 735

Answers (2)

Kartik
Kartik

Reputation: 2609

You need to add a hidden key for productDetail.id or else it considers product detail object to be a transient one and saves it as a new object instead of updating it.

Upvotes: 0

ScanQR
ScanQR

Reputation: 3820

This is because you are using primitive data type int. Update it to Integer and it should work fine.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;

Update it to,

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

NOTE : Regenerate the corresponding getter/setters for this field.

Upvotes: 1

Related Questions