Home > Mobile >  Weird behaviour of Hibernate when saving an Entity with @Convert classes
Weird behaviour of Hibernate when saving an Entity with @Convert classes

Time:08-08

So i have a Promotion Domain with classes like Entity, Service, Controller and so on.

But i have an interface property in Entity class, and based on parameters passed at POST time, that instance is gonna be a subclass saved in the DB. BUT i encountered a weird behaviour of Hibernate in regards of this. If i put that property to be promotionSeason = "easterPromotion" - first, in console, appers to be created as a ChristmasPromotionSeason and then updated as an EasterPromotionSeason, i don't know why, if i make another example, let's say: promotionSeason = "noPromotion", same problem...

I get the wrong result on Query to see what Promotion we have based on PromotionSeason, if i have EasterPromotion saved in the DB, is gonna returl ChristmasPromotion in Query result, why is this problem occur? You can see that on Hibernate console log from below...

Promotion Entity:

@Entity
@org.hibernate.annotations.DynamicInsert
@org.hibernate.annotations.DynamicUpdate
@Access(AccessType.FIELD)
public class Promotion {

    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) //pre Insert values
    private Long promotionId;

    //Strategy Pattern, maybe State pattern was more suitable?
    //check this -> https://stackoverflow.com/questions/51138344/hibernate-persisting-a-composition-interface-of-strategy-pattern
    @Convert(converter = PromotionConverter.class)
    @Column(name = "PROMOTION_SEASON", nullable = false)
    private PromotionSeason promotionSeason;

    public Promotion() {}

    public Promotion(PromotionSeason promotionSeason)
    {
        this.promotionSeason = promotionSeason;
    }
    public Long getPromotionId() {
        return promotionId;
    }

    public PromotionSeason getPromotionSeason() {
        return promotionSeason;
    }

    public void setPromotionSeason(PromotionSeason promotionSeason) {
        this.promotionSeason = promotionSeason;
    }
}

Promotion Service

@Service
public class PromotionService {

    private final PromotionRepository promotionRepository;
    private final PromotionCallingOthers promotionCallingOthers;

    @PersistenceContext
    private EntityManager entityManager;



    @Autowired
    public PromotionService(PromotionRepository promotionRepository, PromotionCallingOthers promotionCallingOthers) {
        this.promotionRepository = promotionRepository;
        this.promotionCallingOthers = promotionCallingOthers;
    }

    public void createPromotion(Promotion promotion)
    {

        System.out.println(promotion.getPromotionSeason().isSeason());
        this.promotionRepository.save(promotion);
    }

    public void addProducts(Promotion promotion, String productName) {
        Promotion createdPromotion = new Promotion(promotion.getPromotionSeason());

        ResponseEntity<Product> productResponseEntity = promotionCallingOthers.callProduct(productName);
        Session session = entityManager.unwrap(Session.class);
        session.update(productResponseEntity.getBody()); // for detached entity error

        createdPromotion.addProduct(productResponseEntity.getBody());

        double price = createdPromotion.getProductList().get(0).getProductPrice();
        double discountedPrice = createdPromotion.getPromotionSeason().applySeasonPromotionDiscount(price);


        double priceTo = getDigitsFormat(price - discountedPrice);
        Objects.requireNonNull(productResponseEntity.getBody()).setProductPrice(priceTo);

        createdPromotion.setNumberOfProductsAtPromotion(productResponseEntity.getBody().getProductQuantity());
        this.promotionRepository.save(createdPromotion);

    }

    private double getDigitsFormat(double numberToFormat)
    {
        DecimalFormat formatDecimal = new DecimalFormat("#.##");
        return Double.parseDouble(formatDecimal.format(numberToFormat));
    }

    public Promotion createPromotionWithType(String promotionType) {
        Promotion promotion = new Promotion();
        promotion.setPromotionSeason(setPromotionSeasonImplBasedOnType(promotionType));
        promotionRepository.save(promotion);
        return promotion;
    }

    public Promotion getPromotionSeasonBasedOnSomething(String promotionType)
    {
        PromotionSeason promotionSeason = setPromotionSeasonImplBasedOnType(promotionType);
        Promotion promotion = promotionRepository.findPromotionByPromotionSeason(promotionSeason);
        System.out.println(promotion.getPromotionSeason());
        return promotion;
    }

    private PromotionSeason setPromotionSeasonImplBasedOnType(String promotionType)
    {
        // eh, state pattern would be better i guess
        switch (promotionType.toLowerCase()) {
            case "christmas":
                return new PromotionChristmasSeason();
            case "easter":
                return new PromotionEasterSeason();
            default:
                return new NoPromotionForYouThisTimeMUHAHA();
        }
    }




    public Promotion test(String testam) {
        PromotionSeason promotionSeason = checkPromotionSeason(testam);
      
        System.out.println(promotionSeason.isSeason());

        
        Promotion promotion = promotionRepository.findWhatPromotionSeasonWeHave(promotionSeason);


        if (promotion == null) {

            System.out.println("promotion season ii in if: "   promotionSeason.isSeason());
            Promotion promotion1 = new Promotion(promotionSeason);
            System.out.println(promotion1.getPromotionSeason().isSeason());
            //?
            promotion = promotion1;
            System.out.println(promotion.getPromotionSeason().isSeason());

            promotion.setPromotionStore(promotion1.getPromotionStore());
          
            promotionRepository.save(promotion);
            System.out.println(promotion.getPromotionSeason().isSeason());
            return promotion;
        }

      
        promotion.setPromotionSeason(promotionSeason); 
        promotionRepository.save(promotion);
        System.out.println("promotion is"   promotion.getPromotionSeason().isSeason());
        return promotion;
    }

    private PromotionSeason checkPromotionSeason(String promotionSeason)
    {
        System.out.println("is \n"   promotionSeason.toLowerCase());
        switch (promotionSeason.toLowerCase().trim())
        {
            case "easter" :
                return new PromotionEasterSeason();
            case "christmas" :
                return new PromotionChristmasSeason();

            default:
                return new NoPromotionForYouThisTimeMUHAHA();
        }
    }

Promotion Repository:

@Repository
public interface PromotionRepository extends JpaRepository<Promotion, Long> {

    @Query("SELECT s FROM Promotion s WHERE s.promotionSeason = :promotionSeason")
    Promotion findWhatPromotionSeasonWeHave(@Param("promotionSeason") PromotionSeason promotionSeason);
    Promotion findPromotionByPromotionSeason(PromotionSeason promotionSeason);
}

Promotion Controller:

@RestController
@RequestMapping(value = "/promotions")
public class PromotionController {

    private final PromotionService promotionService;

    @Autowired
    public PromotionController(PromotionService promotionService) {
        this.promotionService = promotionService;
    }

    @PostMapping(value = "/createPromotion")
    public ResponseEntity<String> createPromotion(@RequestBody Promotion promotion)
    {
        promotionService.createPromotion(promotion);
        return ResponseEntity.status(HttpStatus.CREATED)
                .body("Done");
    }

    @GetMapping(value = "/createPromotion/{promotionType}")
    public ResponseEntity<Promotion> createPromotionType(@PathVariable String promotionType)
    {

        return ResponseEntity.status(HttpStatus.CREATED).body(promotionService.createPromotionWithType(promotionType));
    }

    //WRONG RESULT
    @GetMapping(value = "/getPromotion/{promotionType}")
    public ResponseEntity<Promotion> getPromotionType(@PathVariable String promotionType)
    {
        return ResponseEntity.status(HttpStatus.FOUND).body(promotionService.getPromotionSeasonBasedOnSomething(promotionType));
    }

   
    //WRONG RESULT
    @GetMapping(value = "/test/{promotion}")
    public Promotion check(@PathVariable String promotion)
    {
        return promotionService.test(promotion);
    }
}

PromotionSeason Interface:

//see: -> https://www.youtube.com/watch?v=IlLC3Yetil0
//see: -> https://stackoverflow.com/questions/72155637/a-way-of-polymorphic-http-requests-using-postman/72158992#72158992

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "promotion")
@JsonSubTypes(
        {       @JsonSubTypes.Type(value = PromotionEasterSeason.class, name = "easterPromotion"),
                @JsonSubTypes.Type(value = PromotionChristmasSeason.class, name = "christmasPromotion"),
                @JsonSubTypes.Type(value = NoPromotionForYouThisTimeMUHAHA.class, name = "noPromotion")
        })
public interface PromotionSeason {

    String isSeason();

    double applySeasonPromotionDiscount(double initialPrice);
}

PromotionEasterSeason class - Impl of Interface:

@JsonTypeName(value = "easterPromotion")
public class PromotionEasterSeason implements PromotionSeason{

    private double promotionProcentToDiscount = 10.99f;

    @Override
    public String isSeason() {
        return "Is Easter Season Discount Time of the Year again!";
    }

    @Override
    public double applySeasonPromotionDiscount(double initialPrice) {
        System.out.println("Now you have to pay less with: "   calculateDiscount(initialPrice)   ", instead of: "   initialPrice);
        return calculateDiscount(initialPrice);
    }

    private double calculateDiscount(double initialPriceToDiscount)
    {
        return this.promotionProcentToDiscount  / initialPriceToDiscount;
    }
}

PromotionConverter Class:

public class PromotionConverter implements AttributeConverter<PromotionSeason, String> {

    @Override
    public String convertToDatabaseColumn(PromotionSeason attribute) {
        return attribute.getClass().getSimpleName().trim().toLowerCase(Locale.ROOT);
    }

    @Override
    public PromotionSeason convertToEntityAttribute(@NotBlank String dbData) {
        return stateOfPromotion(dbData);
    }

    private PromotionSeason stateOfPromotion(String state)
    {
        return state.equals("easterPromotion") ? new PromotionEasterSeason() : new PromotionChristmasSeason();
    }
}

Hibernate SQL Console:

2022-08-07 12:16:37.123 DEBUG 7432 --- [nio-8080-exec-2] org.hibernate.SQL                        : call next value for hibernate_sequence
2022-08-07 12:16:37.249 DEBUG 7432 --- [nio-8080-exec-2] org.hibernate.SQL                        : insert into PROJECT_HIBERNATE_Promotion (NUMBER_PRODUCTS_AT_PROMOTION, PROMOTION_SEASON, promotionId) values (?, ?, ?)
2022-08-07 12:16:37.258 TRACE 7432 --- [nio-8080-exec-2] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [INTEGER] - [0]
2022-08-07 12:16:37.260 DEBUG 7432 --- [nio-8080-exec-2] tributeConverterSqlTypeDescriptorAdapter : Converted value on binding : com.shoppingprojectwithhibernate.PromotionsModule.Domain.PromotionChristmasSeason@54de6f66 -> promotionchristmasseason
2022-08-07 12:16:37.260 TRACE 7432 --- [nio-8080-exec-2] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [promotionchristmasseason]
2022-08-07 12:16:37.261 TRACE 7432 --- [nio-8080-exec-2] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
2022-08-07 12:16:37.271 DEBUG 7432 --- [nio-8080-exec-2] org.hibernate.SQL                        : update PROJECT_HIBERNATE_Promotion set PROMOTION_SEASON=? where promotionId=?
2022-08-07 12:16:37.273 DEBUG 7432 --- [nio-8080-exec-2] tributeConverterSqlTypeDescriptorAdapter : Converted value on binding : com.shoppingprojectwithhibernate.PromotionsModule.Domain.PromotionEasterSeason@18c401ef -> promotioneasterseason
2022-08-07 12:16:37.274 TRACE 7432 --- [nio-8080-exec-2] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [promotioneasterseason]
2022-08-07 12:16:37.275 TRACE 7432 --- [nio-8080-exec-2] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]

CodePudding user response:

What the methods in your class PromotionConverter do is inconsistent:

convertToDatabaseColumn will convert to the name of the promotion season class in lowercase, so it will return: promotioneasterseason or promotionchristmasseason or nopromotionforyouthistimemuhaha. This is the string that will be stored in the database.

But the method convertToEntityAttribute, which is supposed to do the opposite, does something else: if the string from the database is easterPromotion then it will return an object of type PromotionEasterSeason and otherwise an object of type PromotionChristmasSeason.

So, what happens when you save an easter promotion in the database: Hibernate will store the string promotioneasterseason. And when you read that record back from the datase, the converter notices that that does not match easterPromotion so it will return a PromotionChristmasSeason.

Solution: Make sure the methods convertToDatabaseColumn and convertToEntityAttribute do the opposite of eachother.

  • Related