Omar Abdelhady
Omar Abdelhady

Reputation: 1738

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

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.

Upvotes: 0

Views: 2981

Answers (2)

Omar Abdelhady
Omar Abdelhady

Reputation: 1738

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.

Upvotes: 2

Christian Beikov
Christian Beikov

Reputation: 16452

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;

}

Upvotes: 1

Related Questions