Issue
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]
Solution
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.
Answered By - Jesper
Answer Checked By - Mary Flores (JavaFixing Volunteer)