Error when mapping classes with attribute that has foreign key - java

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;
}

Related

modelMapper's skip() method always return null

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 :

How to add fields in audit table using Envers in Spring Boot

Good morning, how can I add fields in my audit table?
I need to audit some tables, but I need to get the user who did the operation. My entity who will be audited is:
#Entity
#Table(name ="TableName")
#Audited
#AuditTable("TableNameAuditedLog")
public class MyEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "myId")
private Long id;
#Column(name = "myName")
private String name;
}
Looking the docs, I saw an example a custom class to be my audit and a listener, so I made like this:
#Data
#RevisionEntity(AuditListener.class)
#MappedSuperclass
public class Audit {
#Id
#GeneratedValue
#RevisionNumber
private Long id;
#RevisionTimestamp
private Long timestamp;
#Column(name = "user")
private String user;
}
public class AuditListener implements RevisionListener {
#Override
public void newRevision(Object revisionEntity) {
Audit audit = (Audit) revisionEntity;
audit.setUsuario("user");
}
}
I've tried to extends my Audit class in my Entity class, but I'd trouble with JPA, the trouble is:
Caused by: org.hibernate.MappingException: Unable to find column with logical name: myId in org.hibernate.mapping.Table(TableNameAuditedLog) and its related supertables and secondary tables
How can I do this?
Thank you all.
Remove the MappedSuperClass from your Audit class. You could also have Audit extend DefaultRevisionEntity. All you would have in Audit class is your custom field.
#Column(name = "user")
private String user;
A custom audit revision entity:
#Entity
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#RevisionEntity(UserRevisionListener.class)
public class AuditRevisionEntity extends DefaultTrackingModifiedEntitiesRevisionEntity {
private static final long serialVersionUID = 1L;
private Long userId;
#Column(length = 100, nullable = false)
private String initiator;
}
And revision listener
public class UserRevisionListener implements RevisionListener {
private static final String SYSTEM_USER = "System";
private transient final SecurityUtils securityUtils;
public UserRevisionListener(final SecurityUtils securityUtils) {
this.securityUtils = securityUtils;
}
#Override
public void newRevision(Object revisionEntity) {
final AuditRevisionEntity are = (AuditRevisionEntity) revisionEntity;
securityUtils.getPrincipal().ifPresentOrElse((appPrincipal) -> {
are.setUserId(appPrincipal.getUserId());
are.setInitiator(appPrincipal.getDisplayName());
}, () -> are.setInitiator(SYSTEM_USER));
}
}
In my case I am getting the current principal(I am using a custom principal that has the extra fields) using a SecurityUtils helper and setting the AuditRevisionEntity as needed. Some changes are made by Quartz jobs so there is no principal in which case only the initiator is set.

Getting nulls while using ModelMapper

I'm trying to utilize the ModelMapper in my convertion process. What I need to do is to convert the Sample entity to SampleDTO object.
I have the Sample entity like the following:
#Entity
#Table(name = "sample", schema = "sample_schema")
#Data
#NoArgsConstructor
public class Sample {
private static final String SEQUENCE = "SAMPLE_SEQUENCE";
#Id
#SequenceGenerator(sequenceName = SEQUENCE, name = SEQUENCE, allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
private Long id;
#Column(name = "name")
private String name;
#Column
private String surname;
#OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "id_deetails")
private Details details;
}
Which holds the Details one:
#Entity
#Table(name = "details", schema = "sample_schema")
#Data
#NoArgsConstructor
public class Details {
private static final String SEQUENCE = "DETAILS_SEQUENCE";
#Id
#SequenceGenerator(sequenceName = SEQUENCE, name = SEQUENCE, allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
private Long id;
#Column(name = "street_name")
private String streetName;
#Column
private String city;
}
I'd like the DTO to be this format:
#NoArgsConstructor
#AllArgsConstructor
#Data
public class SampleDTO {
private Long id;
private String name;
private String surname;
private String streetName;
private String city;
}
I also made a ModelMapper bean like:
#Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
And I made a converter component:
#Component
public class EntityDtoConverter {
private final ModelMapper modelMapper;
#Autowired
public EntityDtoConverter(ModelMapper modelMapper) {
this.modelMapper = modelMapper;
}
public SampleDTO sampleToDto(Sample entity) {
return modelMapper.map(entity, SampleDTO.class);
}
}
The problem is
when I try to use this mapper converter in my service
#Service
public class SampleService {
private final SampleRepository sampleRepository;
private final EntityDtoConverter entityDtoConverter;
#Autowired
public SampleService(SampleRepository sampleRepository, EntityDtoConverter entityDtoConverter) {
this.sampleRepository = sampleRepository;
this.entityDtoConverter = entityDtoConverter;
}
public List<SampleDTO> getSamples() {
List<SampleDTO> samples = sampleRepository.findAll()
.map(entityDtoConverter::sampleToDto);
return new List<SampleDTO>(samplesPage);
}
}
I get nulls in places of Details fields.
I have followed Baeldung's tutorial about model-to-dto conversion with ModelMapper and the documentation of it as well but the least wasn't much of help. There is something I'm missing and I have no idea what it is.
I'm working on:
Java 11
Spring Boot 2.3.0
ModelMapper 2.3.8
Try:
modelMapper.getConfiguration().setPropertyCondition(Conditions.isNotNull());
Also check: Modelmapper: How to apply custom mapping when source object is null?

Passing fk in json via rest call as part of DerivedIdentities

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.

Not exposing the path of entities that have a composite primary key to the front end when using the Springframework Page object

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))));

Categories