0

I cannot get @GeneratedValue to work with @IdClass if it includes a foreign key from another entity.

So what I have is an Option entity that looks like this

   @Data
   @NoArgsConstructor
   @AllArgsConstructor
   @EqualsAndHashCode(callSuper = true)
   @Entity
   @Table(name = "options")
   public class Option extends UserDateAudit {
   
       @Id
       @GeneratedValue(strategy = GenerationType.IDENTITY)
       @Column(name = "option_id")
       private Long optionId;
   
       @NotBlank
       @Column(nullable = false)
       private String name;
   
       //one to many with optionValues entity
       @OneToMany(mappedBy = "option", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
       private Set<OptionValue> optionValues;
   
       @OneToMany(mappedBy = "option", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
       private Set<ProductOption> optionProducts;
   
   }

and an OptionValue Entity

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @EqualsAndHashCode(callSuper = true)
    @Entity
    @Table(name = "option_values")
    @IdClass(OptionValueId.class)
    public class OptionValue extends UserDateAudit {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "option_value_id")
        private Long optionValueId;
    
        @NotBlank
        @Column(nullable = false)
        private String valueName;
    
        @Id
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "option_id", referencedColumnName = "option_id")
        private Option option;
    
        @OneToMany(mappedBy = "optionValue", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
        private Set<VariantValue> variantValues;
    
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class OptionValueId implements Serializable {
        private Long optionValueId;
        private Option option;
    }

and I try to save it

    public ResponseEntity<OptionValue> create(Long optionId, OptionValueCreateDto optionValueCreateDto) {
            Option option = optionRepository.findById(optionId).orElseThrow(
                    () -> new EntityNotFoundException("errors.option.notFound")
            );
            OptionValue optionValue = ObjectMapperUtils.map(optionValueCreateDto, OptionValue.class);
            optionValue.setOption(option);
            optionValue = optionValueRepository.save(optionValue);
            return new ResponseEntity<>(optionValue, HttpStatus.CREATED);
        }

but I get the following exception

Resolved [org.springframework.beans.ConversionNotSupportedException: Failed to convert property value of type 'java.lang.Long' to required type 'com.ecommerce.product.model.Option' for property 'option'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.Long' to required type 'com.ecommerce.product.model.Option' for property 'option': no matching editors or conversion strategy found]

and I cannot figure out what is wrong here

I also tried making my IdClass like this

@Data
@AllArgsConstructor
@NoArgsConstructor
public class OptionValueId implements Serializable {
    @Column(name = "option_value_id")
    private Long optionValueId;
    @Column(name = "option_id")
    private Long option;
}

but it did not work as well and showed a similar exception

Edit 1 It turns out it has to be a compound key as this compound key is related used in another table which caused a lot of issues to remove the validation the compound key provides.

maybe I should have clarified that in the first place.

Omar Abdelhady
  • 1,528
  • 4
  • 19
  • 31

2 Answers2

2

So here's the full explanation for what I did.

the Option class stays the same

but updated the OptionValue

1- Option Value entity

package com.ecommerce.product.model;

import com.ecommerce.product.model.helper.OptionValueId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Set;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "option_values")
@IdClass(OptionValueId.class)
@SequenceGenerator(name="OPTION_VALUE", sequenceName="OPTION_VALUE_GENERATOR")
public class OptionValue implements Serializable {

    private static final long serialVersionUID = -1L;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="OPTION_VALUE")
    @Column(name = "option_value_id")
    private Long optionValueId;

    @Id
    @Column(name = "option_id")
    private Long optionId;

    @NotBlank
    @Column(nullable = false)
    private String valueName;

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "option_id", insertable = false, updatable = false)
    @NotNull
    private Option option;

    @OneToMany(mappedBy = "optionValue", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<VariantValue> variantValues;

}

The difference here is the in the @GeneratedValue(). Using the GenerationType.IDENTITY is not valid when using a compound key it has to be GenerationType.SEQUENCE to work well. in this case providing a generator would save you from letting hibernate use its global one which will be shared with all entities using SEQUENCE generator.

@SequenceGenerator(name="OPTION_VALUE", sequenceName="OPTION_VALUE_GENERATOR")

It just doesn't work with IDENTITY. You can check this issue as well as this one.

2- The id class

a simple Id class would be like following


@Embeddable
@Data
@NoArgsConstructor
public class OptionValueId implements Serializable {

    private static final long serialVersionUID = -1L;

    private Long optionValueId;
    private Long optionId;

}

3- Saving the entity

public ResponseEntity<OptionValueDto> create(Long optionId, OptionValueCreateDto optionValueCreateDto) {
        Option option = optionRepository.findById(optionId).orElseThrow(
                () -> new EntityNotFoundException("errors.option.notFound")
        );
        OptionValue optionValue = ObjectMapperUtils.map(optionValueCreateDto, OptionValue.class);
        optionValue.setOption(option);
        optionValue.setOptionId(optionId);
        optionValue = optionValueRepository.save(optionValue);
        return new ResponseEntity<>(ObjectMapperUtils.map(optionValue, OptionValueDto.class), HttpStatus.CREATED);
    }

** NOTE that you provide the optionId as well the option but not the optionValueId

4- The entity that uses the compound Id for a relation

package com.ecommerce.product.model;

import com.ecommerce.product.model.helper.VariantValueId;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.io.Serializable;

@Data
@NoArgsConstructor
@Entity
@Table(name = "variant_values")
@IdClass(VariantValueId.class)
public class VariantValue implements Serializable {

    private static final long serialVersionUID = -1L;

    @Id
    @Column(name = "product_variant_id")
    private Long productVariantId;

    @Id
    @Column(name = "option_id")
    private Long optionId;

    @Id
    @Column(name = "product_id")
    private Long productId;

    @Column(name = "option_value_id")
    private Long optionValueId;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumns({
            @JoinColumn(name = "product_id", referencedColumnName = "product_id", insertable = false, updatable = false),
            @JoinColumn(name = "product_variant_id", referencedColumnName = "product_variant_id", insertable = false, updatable = false)
    })
    private ProductVariant productVariant;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumns({
            @JoinColumn(name = "option_id", referencedColumnName = "option_id", insertable = false, updatable = false),
            @JoinColumn(name = "product_id", referencedColumnName = "product_id", insertable = false, updatable = false)
    })
    private ProductOption productOption;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumns({
            @JoinColumn(name = "option_id", referencedColumnName = "option_id", insertable = false, updatable = false),
            @JoinColumn(name = "option_value_id", referencedColumnName = "option_value_id", insertable = false, updatable = false)
    })
    @NotNull
    private OptionValue optionValue;

}

Saving the last entity would be by providing instances of the compound Ids.

Omar Abdelhady
  • 1,528
  • 4
  • 19
  • 31
1

The exception is related to Spring Data or WebMvc not being able to convert values. Mixing generated identifiers with other identifiers is not really possible. Why do you need both values anyway? It makes IMO no sense to have a composite id here. Just use this:

@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "option_values")
@IdClass(OptionValueId.class)
public class OptionValue extends UserDateAudit {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "option_value_id")
    private Long optionValueId;

    @NotBlank
    @Column(nullable = false)
    private String valueName;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "option_id", referencedColumnName = "option_id")
    private Option option;

    @OneToMany(mappedBy = "optionValue", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<VariantValue> variantValues;

}
Christian Beikov
  • 15,141
  • 2
  • 32
  • 58
  • Well I was just trying to follow this design here https://stackoverflow.com/questions/24923469/modeling-product-variants if you please have some time to review it, as I am not sure if that's the best approach. I am not quite sure why he used a foreign key as primary key specially in the VariantValues table. – Omar Abdelhady Dec 01 '20 at 13:15
  • 1
    As far as the diagram shows you don't have a composite id – Christian Beikov Dec 01 '20 at 18:06
  • thank you for your time, it's just that I am talking about the accepted answer after the diagram of the answer it explains every table OPTION_VALUES PK: option_id, value_id UK: option_id, value_name FK: option_id REFERENCES OPTIONS (option_id) but it doesn't explain why using the foreign key as primary key along with the entity id – Omar Abdelhady Dec 01 '20 at 18:50
  • so I can not really use composite key in anyway here and get it to work with the mapping (part of it is a foreign key and the other one is a unique Id for the entity) and get it to work with @GeneratedValue ? is there any way to make this work – Omar Abdelhady Dec 02 '20 at 05:41
  • as I reviewed the design and found out the composite primary keys will stop you from adding for a non existing product_variant in my case and that's a type of validation I want, is it possible to implement – Omar Abdelhady Dec 02 '20 at 05:42
  • You don't need a composite primary key to validate that. The foreign key is what validates that the target object exists. – Christian Beikov Dec 02 '20 at 10:13
  • well, I tested both designs on pgAdmin and indeed unlike what the answer I mentioned said, a foreign key would accomplish required validation, but I don't really want to leave something that could be done with the database itself that I cannot implement, so any clue what is wrong in my implementation, I mean can we really not do something similar where you have a foreign key as a primary key along with a @GeneratedValue entity Id? or it can be done, so that if it can be done I really want to do it here, get it to work and then remove the composite key as you suggested – Omar Abdelhady Dec 02 '20 at 19:00
  • 1
    It's simply unnecessary to create a primary key based on a sequence value and additional columns. The sequence value is unique already, there is no need for further columns. Please read into basic data modeling as I think you are lacking knowledge about these concepts. – Christian Beikov Dec 03 '20 at 09:05