I got a Product entity that has an embedded key (id,manufacturer). I'm passing through REST call the product entity with fk of the manufacturer :
{
"name":"Chocolate",
"register_date":"19/03/2020",
"manufacturer_id": 52,
"rating":"Amazing"
}
When I try to save the entity in the controller I'm getting the following error
java.lang.IllegalArgumentException: Can not set com.dao.Manufacturer field com.dao.ProductId.manufacturer to java.lang.Long
Product :
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
#IdClass(ProductId.class)
#Entity
public class Product {
#Id
#SequenceGenerator(name = "product_id_seq", sequenceName = "product_id_seq", initialValue = 1)
#GeneratedValue(strategy=GenerationType.SEQUENCE,generator = "product_id_seq")
private Long id;
#ManyToOne(fetch=FetchType.LAZY)
#Id
private Manufacturer manufacturer;
private String name;
#JsonFormat(pattern = "dd/MM/YYYY")
private Date register_date;
#Enumerated(EnumType.ORDINAL)
private Rating rating;
public enum Rating {
Amazing,Good_Value_For_Money,Bad
}
The Id class :
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import java.io.Serializable;
#AllArgsConstructor
#NoArgsConstructor
public class ProductId implements Serializable {
private Long id;
private Manufacturer manufacturer;
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProductId pId1 = (ProductId) o;
if (id != pId1.id) return false;
return manufacturer.getId() == pId1.manufacturer.getId();
}
#Override
public int hashCode() {
return id.hashCode()+manufacturer.getId().hashCode();
}
}
I also created a DTO object that will be passed through the api to the controller :
#Setter
#Getter
#AllArgsConstructor
#NoArgsConstructor
public class ProductCreationDTO {
private String name;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd/mm/yyyy")
private Date register_date;
private Long manufacturer_id;
private Product.Rating rating;
}
The manufacturer :
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
#Entity
public class Manufacturer {
#Id
#SequenceGenerator(name = "manufacturer_id_seq", sequenceName = "manufacturer_id_seq", initialValue = 1)
#GeneratedValue(strategy=GenerationType.SEQUENCE,generator = "manufacturer_id_seq")
private Long id;
private String name;
private String country;
#OneToMany(mappedBy = "manufacturer",fetch = FetchType.LAZY)
private List<Product> products;
In my controller I have the following 2 functions :
RestController
#RequestMapping("/api")
public class ProductController {
#Autowired
ProductService productService;
#Autowired
ManufacturerService manufacturerService;
#RequestMapping(value = "/product/", method = RequestMethod.POST)
public HttpStatus insertProduct(#RequestBody ProductCreationDTO pcd) {
Product p = mapProductCreationDTOtoProduct(pcd);
if(p.getManufacturer() == null)
return HttpStatus.BAD_REQUEST;
return productService.addProduct(p) ? HttpStatus.CREATED : HttpStatus.BAD_REQUEST;
}
private Product mapProductCreationDTOtoProduct(ProductCreationDTO pcd)
{
Product p = new Product();
p.setName(pcd.getName());
p.setRating(pcd.getRating());
p.setRegister_date(pcd.getRegister_date());
Optional<Manufacturer> m = manufacturerService.getManufacturer(pcd.getManufacturer_id());
if (m.isPresent())
p.setManufacturer(m.get());
return p;
}
the add method under the productService :
#Transactional
public boolean addProduct(Product p)
{
return productRepository.save(p)!=null;
}
Update
I followed the following Stack Overflow post. I changed the ProductId to:
public class ProductId implements Serializable {
private Long id;
private Long manufacturer;
....
and in the Product class I added the following annotation above the Manufacturer :
#Id
#ManyToOne(fetch=FetchType.LAZY)
#MapsId("manufacturer")
private Manufacturer manufacturer;
and now I'm getting the following error:
org.hibernate.HibernateException: No part of a composite identifier may be null
Update 2
It looks like the id of the Product isn't populated and that's why it isn't created. I tried setting the id in the following function and the product was inserted successfully:
private Product mapProductCreationDTOtoProduct(ProductCreationDTO pcd)
{
Product p = new Product();
p.setName(pcd.getName());
p.setRating(pcd.getRating());
p.setRegister_date(pcd.getRegister_date());
Optional<Manufacturer> m = manufacturerService.getManufacturer(pcd.getManufacturer());
if (m.isPresent())
p.setManufacturer(m.get());
p.setId((long) 1); <-------------------------------------------
return p;
}
So the open question now is why the id isn't populated ?
The obvious mistake is a #id annotation over the name property in the Product entity whereas it should be over the manufacturer property
My original problem was :
java.lang.IllegalArgumentException: Can not set com.dao.Manufacturer field com.dao.ProductId.manufacturer to java.lang.Long
I solved it by following this post . The bottom line was that inside my IdClass, the type of the composite object should be his PK :
public class ProductId implements Serializable {
private Long id; // matches name of #Id attribute
private Long manufacturer; // name should match to #Id attribute and type of Manufacturer PK
Althought after solving it I faced a new :
org.hibernate.HibernateException: No part of a composite identifier may be null
might be a bug(or this bug) related to hibernate when using #Idclass.
Either way, the way to handle this problem and solve it is initiate the id column to a value :
public class Product {
#Id
#SequenceGenerator(name = "product_id_seq", sequenceName = "product_id_seq")
#GeneratedValue(strategy= GenerationType.SEQUENCE,generator = "product_id_seq")
private Long id=-1L;
This will bypass hibernate validation of the id and allow it to map the sequence value to it afterwards.
Related
I have the Hardware entity, HardwareDtoRequest and HardwareDtoResponse classes, where I'm using the modelMapper to map them. In the Hardware table, there is a foreign key to the Provider table.
The problem is that I am not able to map this attribute to HardwareDtoRequest, when I call the POST method in Postman passing only the provider_id in the request body it saves only one record with that particular ID, when trying to save again another record with the same ID it updates the old one.
How do I map this foreign key attribute to the DtoRequest and save?
Hardware.java
#Getter
#Setter
#Entity
public class Hardware {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 100)
private String name;
#ManyToOne
#JoinColumn(name = "provider_id")
private Provider provider;
}
Provider.java
#Getter
#Setter
#Entity
public class Provider {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 100)
private String name;
}
HardwareDtoRequest.java
#Getter
#Setter
#ToString
#AllArgsConstructor
#NoArgsConstructor
public class HardwareDtoRequest {
#NotNull(message = "required field")
private String name;
#NotNull(message = "required field")
private Long providerId;
}
HardwareDtoResponse.java
#Getter
#Setter
#ToString
#AllArgsConstructor
#NoArgsConstructor
public class HardwareDtoResponse {
private Long id;
private String name;
private ProviderDtoResponse provider;
}
HardwareMapper.java
public HardwareDtoResponse toHardwareDtoResponse(Hardware hardware) {
return mapper.map(hardware, HardwareDtoResponse.class);
}
public Hardware toHardware(HardwareDtoRequest hardwareDtoRequest) {
return mapper.map(hardwareDtoRequest, Hardware.class);
}
HardwareService.java
#Transactional
public HardwareDtoResponse save(HardwareDtoRequest hardwareDtoRequest) {
Hardware hardware = mapper.toHardware(hardwareDtoRequest);
Hardware saveHardware = hardwareRepository.save(hardware);
return mapper.toHardwareDtoResponse(saveHardware);
}
HardwareController.java
#PostMapping
public ResponseEntity<HardwareDtoResponse> save(#Valid #RequestBody HardwareDtoRequest hardwareDtoRequest) {
log.info("saving hardware: {}", hardwareDtoRequest);
HardwareDtoResponse hardware = hardwareService.save(hardwareDtoRequest);
return new ResponseEntity<>(hardware, HttpStatus.CREATED);
}
I managed to solve it, for those who have the same problem of mapping dtos with modelMapper, I use the following snippet in ModelMapperConfig:
#Configuration
public class ModelMapperConfig {
#Bean
public ModelMapper mapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
return modelMapper;
}
}
You can try to set provider manually. Like this:
public Hardware toHardware(HardwareDtoRequest hardwareDtoRequest) {
Hardware hardware = mapper.map(hardwareDtoRequest, Hardware.class);
Provider provider = providerRepository.findById(hardwareDtoRequest.providerId);
hardware.setProvider(provider);
return hardware;
}
I cant set my id to null. because modelMapper's skip() method always return null. I don't know how to fix it. I trying to convert dto to entity.
I trying to convert dto to entity but
SubSectionGroupOptionsEntity skip = skip();
always return null. I am using some configuration.
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.LOOSE).setAmbiguityIgnored(true).setDeepCopyEnabled(true).setSkipNullEnabled(true);
modelMapper.addMappings(new PropertyMap<SubSectionGroupOptionsDTO, SubSectionGroupOptionsEntity>() {
#Override
protected void configure() {
SubSectionGroupOptionsEntity skip = skip();
skip.setId(null);
}
});
QualitySectionMasterEntity qualitySectionMasterEntity = modelMapper.map(sectionSaveDTO, QualitySectionMasterEntity.class);
Look here's my SubSectionGroupOptionsDTO class :
#AllArgsConstructor
#ToString
#Builder
#Data
#NoArgsConstructor
public class SubSectionGroupOptionsDTO {
private Long id;
private String optionName;
private String inputType;
private List<SubSectionGroupOptionActionsDTO> quesAnsGrpOptionAction;
}
and my entity class is :
#NoArgsConstructor
#AllArgsConstructor
#Data
#ToString
#Builder
#Entity
#Table(name = "sub_section_group_options")
public class SubSectionGroupOptionsEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "option_name")
private String optionName;
#Column(name = "input_type")
private String inputType;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "sub_section_group_options_id")
private List<SubSectionGroupOptionActions> subSectionGroupOptionActionsSet;
}
Remember ::: SubSectionGroupOptionsEntity is a deep sub child of
QualitySectionMasterEntity
please look and provide a fixed solution :
so I have an hibernate Entity called Appointment, in this entity I have a AppointNumber property which itself contains a number property which is a string.
When I persist my Appointment, I need the AppointmentNumber. I got it to work with #Embedded and #Embeddable the other day but this creates a join table Which I can't have.
I tried many other solutions to try and get it to work without join tables but I can't figure it out. (I get lots of ava.lang.IllegalStateException)
Can anyone help?
Thanks!
#Entity(name = "appointments")
public class Appointment {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#OneToOne(mappedBy = "number")
#Fetch(value = FetchMode.SELECT)
private AppointmentNumber appointmentNumber;
Appointment entity
AppointmentNumber, used in Appointment but should not be an entity
public class AppointmentNumber {
#OneToOne
#JoinColumn(name = "appointmentNumber", unique = true, nullable = false)
private String number;
You could do like this:
#Entity(name = "appointments")
public class Appointment {
///....
#Convert(converter = AppointmentNumberConverter.class)
private AppointmentNumber appointmentNumber;
///....
}
#Converter
public class AppointmentNumberConverter implements
AttributeConverter<PersonName, String> {
#Override
public String convertToDatabaseColumn(AppointmentNumber appointmentNumber) {
if (appointmentNumber == null) {
return null;
}
return appointmentNumber.getNumber();
}
#Override
public AppointmentNumber convertToEntityAttribute(String appointmentNumber) {
if (appointmentNumber == null) {
return null;
}
AppointmentNumber result = new AppointmentNumber();
result.setNumber(appointmentNumber);
return result;
}
}
Have a look at JPA Converter documentation.
I'm working on an API endpoint that returns a Springframework Page response. I want the front end to be able to sort the data but I can't expect the front end to know that the column they want to sort on is actually inside a composite primary key.
In the example below (a simplified version of what I'm working on) you can see that the startDate column is inside a RouteEntityPk class, which is linked to the RouteEntity class with the #EmbeddedId annotation. To Sort on that column the front end would need to add ?sort=pk.startdate,asc to the request. I want the front end to only have to provide ?sort=startdate,asc.
Is there a way - using Spring magic - of having the repository know that startdate == pk.startdate, or will I have to write a translator which will remove the pk when showing the sort column to the front end, and add it where necessary when reading it from the request?
Controller:
#GetMapping(value = "routes/{routeId}", produces = APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<Page<Route>> getRouteByRouteId(#PathVariable(value = "routeId") final String routeId,
#PageableDefault(size = 20) #SortDefault.SortDefaults({
#SortDefault(sort = "order", direction = Sort.Direction.DESC),
#SortDefault(sort = "endDate", direction = Sort.Direction.DESC)
}) final Pageable pageable) {
return ResponseEntity.ok(routeService.getRouteByRouteId(routeId, pageable));
}
Service:
public Page<Route> getRouteByRouteId(String routeId, Pageable pageable) {
Page<RouteEntity> routeEntities = routeRepository.findByRouteId(routeId, pageable);
return new PageImpl<>(
Collections.singletonList(routeTransformer.toRoute(routeId, routeEntities)),
pageable,
routeEntities.getContent().size()
);
}
Repository:
#Repository
public interface RouteRepository extends JpaRepository<RouteEntity, RouteEntityPk> {
#Query(value = " SELECT re FROM RouteEntity re"
+ " AND re.pk.routeId = :routeId")
Page<RouteEntity> findByRouteId(#Param("routeId") final String routeId,
Pageable pageable);
}
Entities:
Route:
#Data
#Entity
#Builder(toBuilder = true)
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "ROUTE", schema = "NAV")
public class RouteEntity {
#EmbeddedId
private RouteEntityPk pk;
#Column(name = "NAME")
private String name;
#Column(name = "ORDER")
private Integer order;
#Column(name = "END_DTE")
private LocalDate endDate;
}
RoutePk:
#Data
#Builder(toBuilder = true)
#Embeddable
#NoArgsConstructor
#AllArgsConstructor
public class RouteEntityPk implements Serializable {
private static final long serialVersionUID = 1L;
#Column(name = "ROUTE_ID")
private String routeId;
#Column(name = "STRT_DTE")
private LocalDate startDate;
}
Models:
Route:
#Data
#Builder
public class Route {
public String name;
public String routeId;
public List<RouteItem> items;
}
Item:
#Data
#Builder
public class Item {
public Integer order;
public LocalDate startDate;
public LocalDate endDate;
}
Transformer:
public Route toRoute(String routeId, Page<RouteEntity> routeEntities) {
return Route.builder()
.name(getRouteName(routeEntities))
.routeId(routeId)
.items(routeEntities.getContent().stream()
.map(this::toRouteItem)
.collect(Collectors.toList()))
.build();
}
private Item toRouteItem(RouteEntity item) {
return ParcelshopDrop.builder()
.order(item.getOrder())
.startDate(item.getStartDate())
.endDate(item.getEndDate())
.build();
}
So it looks like the way to do this is to use the other way you can deal with composite primary key's in JPA, the annotation #IdClass. This way you can put the fields in the main entity and refer to them as such.
Below is a link to the baeldung article I followed and the changes to the entities I posted above that make this work:
https://www.baeldung.com/jpa-composite-primary-keys
Entities:
Route:
#Data
#Entity
#Builder(toBuilder = true)
#NoArgsConstructor
#AllArgsConstructor
#IdClass(RouteEntityPk.class)
#Table(name = "ROUTE", schema = "NAV")
public class RouteEntity {
#Id
#Column(name = "ROUTE_ID")
private String routeId;
#Id
#Column(name = "STRT_DTE")
private LocalDate startDate;
#Column(name = "NAME")
private String name;
#Column(name = "ORDER")
private Integer order;
#Column(name = "END_DTE")
private LocalDate endDate;
}
RoutePk:
#Data
#Builder(toBuilder = true)
#NoArgsConstructor
#AllArgsConstructor
public class RouteEntityPk implements Serializable {
private static final long serialVersionUID = 1L;
private String routeId;
private LocalDate startDate;
}
This is one solution, probably not the best, but you can transform Pageable object in order to replace the field name like this :
In your controller getRouteByRouteId method :
List<Order> orders = pageable.getSort().stream().map(o -> o.getProperty().equals("startdate") ? new Order(o.getDirection(), "pk.startdate"): o).collect(Collectors.toList());
Then you can call the service with the modified object :
return ResponseEntity.ok(routeService.getRouteByRouteId(routeId, PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(orders))));
We have structure something like bellow:
#MappedSuperclass
public abstract class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private Long id;
#Override
public boolean equals(Object o) {
if (this == o) return true;
BaseEntity that = (BaseEntity) o;
if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) {
return false;
}
return true;
}
#Override
public int hashCode() {
return Objects.hash(getId());
}
}
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#Table(name = "BASE_ORGANIZATION")
#DiscriminatorColumn(name = "disc")
#DiscriminatorValue("baseOrganization")
public class BaseOrganization extends BaseEntity {
#Column(name = "TITLE")
private String title;
}
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#Table(name = "BASE_PERSONNEL")
#DiscriminatorColumn(name = "disc")
#DiscriminatorValue("basePersonnel")
public class BasePersonnel extends BaseEntity {
#Column(name = "FIRSTNAME")
private String firstName;
#Column(name = "LASTNAME")
private String lastName;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "BASE_ORGANIZATION_ID")
private BaseOrganization baseOrganization;
}
BaseEntity, BasePersonnel and BaseOrganization is in core_project that all projects can used these objects. Now we create a project that depended to core_project. We must to extends BasePeronnel and BaseOrganization based on our business context. For this reason we added some classes in our project like bellow:
#Entity
#DiscriminatorValue("organization")
public class Organization extends BaseOrganization {
#Column(name = "MISSION_Type")
private String missionType;
}
#Entity
#DiscriminatorValue("personnel")
public class Personnel extends BasePersonnel {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "MISSION_ORGANIZATION_ID")
private Organization missionOrganization;
}
Our problem is raised, when we wanna to get all personnel. When we called getAllPersonnel method in PersonnelRepository, hibernate logged bellow message:
WARN o.h.e.i.StatefulPersistenceContext - HHH000179: Narrowing proxy to class org.xyz.organization.Organization - this operation breaks ==
After that, when we see List<Personnel> object, missionOrganization property is null, but baseOrganization property in super class is loaded!
We think when tow class that have SINGLE_TABLE inheritance strategy, hibernate LazyInitializer can not detect correct concrete class.
Also we debugged narrowProxy method in StatefulPersistenceContext class and we understood that concreteProxyClass.isInstance(proxy) returned false. because proxy object have BaseOrganization object in LazyInitializer and concreteProxyClass refer to Organization class!