I have a composite primary key made of planId and planDate, when the user gives me both this attributes I cant find a way to retrieve it from my Repo. Should findById work like this?
public Plans assignPlansToMeds(Long id, Long planId, Date planDate) {
Set<Meds> medsSet = null;
Meds meds = medsRepo.findById(id).get();
Plans plans = plansRepo.findById(planId, planDate).get();
medsSet = plans.getAssignedMeds();
medsSet.add(meds);
plans.setAssignedMeds(medsSet);
return plansRepo.save(plans);
}
My Primary Key:
#Data
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode
#Embeddable
public class PlansPKId implements Serializable {
private long planId;
private Date planDate; // format: yyyy-mm-dd
}
Plans entity:
#Data
#AllArgsConstructor
#NoArgsConstructor
#Entity
#Table(name = "plans")
public class Plans {
#EmbeddedId
private PlansPKId plansPKId;
#Column
private String planName;
#Column
private String weekday;
#ManyToMany
#JoinTable(name = "Plan_Meds", joinColumns = {
#JoinColumn(name = "planDate", referencedColumnName = "planDate"),
#JoinColumn(name = "planId", referencedColumnName = "planId") }, inverseJoinColumns = #JoinColumn(name = "id"))
private Set<Meds> assignedMeds = new HashSet<>();
}
where I ask for the planId and planDate:
#PutMapping("/medicine/{id}/assignToPlan/{planId}/date/{plandate}")
public Plans assignMedToPlan(#PathVariable Long id, #PathVariable Long planId, #PathVariable Date planDate){
return assignService.assignPlansToMeds(id, planId, planDate);
}
The Spring JpaRepository only allows one type as the ID-type, as can be seen in the javadoc. Therefore, findById will never accept two arguments.
You need to define your repository with your EmbeddedId-type as ID-type as follows:
#Repository
public interface PlansRepository extends JpaRepository<Plans, PlansPKId> {
}
You can then call the findById method as follows:
Plans plans = plansRepo.findById(new PlansPKId(planId, planDate))
.orElseThrow(PlansNotFoundException.idAndDate(planId, planDate));
If you dont want to create a new PlansPKId instance for every query, you could also define a repository method as follows and let Spring derive the query based on the method name:
Optional<Plans> findByPlansPKIdPlanIdAndPlansPKIdPlanDate(long planId, Date planDate);
If you don't like to have a cumbersome method name you could as well define a JPQL query and name the method as you like:
#Query("select p from Plans p where p.plansPKId.planId = :planId and p.plansPKId.planDate = :planDate")
Optional<Plans> findByCompositeId(#Param("planId) long planId, #Param("planDate") Date planDate);
On a side note, I strongly encourage you to use LocalDate, LocalDateTime or ZonedDateTime (depending on your needs) instead of the legacy Date class.
Moreover, you shouldn't call get() on an Optional without checking if it is present. I recently wrote an answer on SO describing how you can create an elegant error handling. If you stuck to my example, you had to create a NotFoundException and then create the PlansNotFoundException which extends NotFoundException. by this means, everytime when a PlansNotFoundException is thrown in thread started by a web request, the user would receive a 404 response and a useful message if you implement it like this:
public abstract class NotFoundException extends RuntimeException {
protected NotFoundException(final String object, final String identifierName, final Object identifier) {
super(String.format("No %s found with %s %s", object, identifierName, identifier));
}
protected NotFoundException(final String object, final Map<String, Object> identifiers) {
super(String.format("No %s found with %s", object,
identifiers.entrySet().stream()
.map(entry -> String.format("%s %s", entry.getKey(), entry.getValue()))
.collect(Collectors.joining(" and "))));
}
}
public class PlansNotFoundException extends NotFoundException {
private PlansNotFoundException(final Map<String, Object> identifiers) {
super("plans", identifiers);
}
public static Supplier<PlansNotFoundException> idAndDate(final long planId, final Date planDate) {
return () -> new PlansNotFoundException(Map.of("id", id, "date", date));
}
}
For the Meds case:
public class MedsNotFoundException extends NotFoundException {
private MedsNotFoundException(final String identifierName, final Object identifier) {
super("meds", identifierName, identifier);
}
public static Supplier<MedsNotFoundException> id(final long id) {
return () -> new MedsNotFoundException("id", id);
}
}
Meds meds = medsRepo.findById(id).orElseThrow(MedsNotFoundException.id(id));
Related
First, I apologize if my english is unclear ; I am french.
I also am a very junior developer, and this is my first real personal project with no tutorial or whatsoever.
I am having some trouble with my Rest api.
I use java 11 and Spring/JPA
I have two DO classes that each represent a table in the database : Artist and Country.
An artist can have several nationalities, and a country can have several artist born in it.
So that means : many to many.
I joined them with an Association table ; ArtistNationality, that is also a class.
I know I could do without an additional class but, since in my app some relations also have some extra-fields (like the year of an award) I decided that all many to many relationships would be materialized the same way, by "join"classes (sorry I really have an hard time to explain in english)
When I create an Artist, I want my response json to contain the created artist with all its nationalities. But it always comes null.
The creation works fine. But here is the response :
What is odd is the results of my API call.
Here is the result of POST method :
{
"artistFirstName": "OH",
"artistLastName": "Test",
"artistBiography": "Je suis un test.",
"artistBirthDate": "1380-11-10",
"artistDeathDate": "1500-11-12",
"artistNationalities": [],
"artist_ID": 3
}
As you can see, nationalities come null, always.
What is expected, is the same as when I do a find or findall :
{
"artistFirstName": "OH",
"artistLastName": "Test",
"artistBiography": "Je suis un test.",
"artistBirthDate": "1380-11-10",
"artistDeathDate": "1500-11-12",
"artistNationalities": [
{
"nationality": {
"countryId": 1,
"countryName": "Andorre",
"countryShortCode": "AD",
"countryFlagFileName": "ad_16.png"
},
"nationalityId": 5
},
{
"nationality": {
"countryId": 12,
"countryName": "Autriche",
"countryShortCode": "AT",
"countryFlagFileName": "at_16.png"
},
"nationalityId": 6
}
],
"artist_ID": 3
}
What I don't understand is that my save method returns the result of a "find" method so why isn't it the same ?? Find and findall work perfectly, and the insertion also works fine.
Here are the DataObject classes, I shortened them to leave only the fields related to question but of course they all come with their constructors and getters/setter stuff :
Artist class :
#Entity
#Table(name = "artist")
public class Artist implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id_artist")
private final Integer ARTIST_ID;
#OneToMany(targetEntity = ArtistNationality.class, mappedBy = "artistIdAsForeignKey", cascade = CascadeType.ALL)
#JsonManagedReference
private List<ArtistNationality> artistNationalities;
Artist DTO
public class ArtistDto {
private final Integer ARTIST_ID;
private String artistFirstName;
private String artistLastName;
private String artistBiography;
private String artistBirthDate;
private String artistDeathDate;
private List<NationalityDto> artistNationalities;
Country class :
#Entity
#Table(name = "country")
public class Country implements Serializable {
#Id
#Column(name = "id_country")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private final Integer COUNTRY_ID;
#OneToMany(targetEntity = ArtistNationality.class, mappedBy = "countryIdAsForeignKey", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JsonBackReference
private List<ArtistNationality> artistsComingFromCountry;
** COUNTRY DTO **
public class CountryDto {
private int countryId;
private String countryName;
private String countryShortCode;
private String countryFlagFileName;
ArtistNationality class :
#Entity
#Table(name="artist_x_nationality")
public class ArtistNationality implements Serializable {
#Id
#GeneratedValue
#Column(name="id_nationality")
private final Integer NATIONALITY_ID;
#ManyToOne (fetch = FetchType.LAZY)
#JsonBackReference
#JoinColumn(name= "fk_nationality_to_artist")
private Artist artistIdAsForeignKey;
#ManyToOne (fetch = FetchType.LAZY)
#JsonBackReference
#JoinColumn(name="fk_nationality_to_country")
private Country countryIdAsForeignKey;
Nationality Dto:
public class NationalityDto {
private final Integer NATIONALITY_ID;
private CountryDto nationality;
ArtistService :
#Service
public class ArtistServiceImpl implements IArtistService {
#Autowired
private IArtistDao artistDao;
#Autowired
private IArtistDoDtoMapper mapper;
#Autowired
private IArtistValidator validator;
#Autowired
private IArtistNationalityDao nationalityDao;
#Override
#Transactional(readOnly = true)
public List<ArtistDto> findAll() {
List<ArtistDto> resultList = new ArrayList<ArtistDto>();
List<Artist> artistsFromDatabase = artistDao.findAll();
if (artistsFromDatabase != null && !artistsFromDatabase.isEmpty()) {
resultList = mapper.mapDoListToDto(artistsFromDatabase);
}
return resultList;
}
#Override
#Transactional(readOnly = true)
public ArtistDto find(final int id) {
Optional<Artist> optArtistFromDatabase = artistDao.findById(id);
if (!optArtistFromDatabase.isPresent()) {
throw new ResourceNotFoundException();
}
Artist artistFromDatabase = optArtistFromDatabase.get();
ArtistDto result = mapper.mapDoToDto(artistFromDatabase);
return result;
}
#Override
#Transactional(rollbackFor = Exception.class)
public ArtistDto save(final ArtistDto objDto) {
if(validator.isValidForDatabase(objDto)){
//Save without nationalities (mapper does not map it);
Artist artistToSave = mapper.mapDtoToDo(objDto);
int artistId = artistDao.save(artistToSave).ARTIST_ID();
//Add the id of the artist to all his nationalities, then save the nationalities
for (NationalityDto nationality : objDto.getArtistNationalities()){
ArtistNationality doNationality = new ArtistNationality(nationality.getNationalityId());
doNationality.setArtistIdAsForeignKey(new Artist(artistId));
doNationality.setCountryIdAsForeignKey(new Country(nationality.getNationality().getCountryId()));
nationalityDao.save(doNationality);
}
ArtistDto returnArtist = this.find(artistId);
return returnArtist;
}
throw new InsertionException("Invalid object. Could not insert into database.");
}
Mappers :
#Override
#Transactional(rollbackFor = Exception.class)
public ArtistDto mapDoToDto(final Artist pDataObject) {
ArtistDto artistDto = new ArtistDto(pDataObject.ARTIST_ID());
if (artistValidator.isNotNullAndNotEmpty(pDataObject.getArtistFirstName())) {
artistDto.setArtistFirstName(pDataObject.getArtistFirstName());
}
if (artistValidator.isOptionPresent(pDataObject.getArtistLastName())) {
artistDto.setArtistLastName(pDataObject.getArtistLastName());
}
if (artistValidator.isOptionPresent(pDataObject.getArtistBiography())) {
artistDto.setArtistBiography(pDataObject.getArtistBiography());
}
if (artistValidator.isOptionPresent(pDataObject.getArtistBirthDate())) {
artistDto.setArtistBirthDate(pDataObject.getArtistBirthDate());
}
if (artistValidator.isOptionPresent(pDataObject.getArtistDeathDate())) {
artistDto.setArtistDeathDate(pDataObject.getArtistDeathDate());
}
List<NationalityDto> artistNationalities = new ArrayList<NationalityDto>();
if (artistValidator.isOptionPresent(pDataObject.getArtistNationalities())) {
System.out.println("yes, we're in !");
for (ArtistNationality nationality : pDataObject.getArtistNationalities()) {
NationalityDto nDto = new NationalityDto(nationality.getNationalityId());
CountryDto cDto = countryMapper.mapDoToDto(nationality.getCountryIdAsForeignKey());
nDto.setNationality(cDto);
artistNationalities.add(nDto);
}
}
artistDto.setArtistNationalities(artistNationalities);
return artistDto;
}
/**
* Note : we add nationality separately since we do not have Artist's ID yet.
*/
#Override
#Transactional(rollbackFor = Exception.class)
public Artist mapDtoToDo(final ArtistDto pDataTransfertObject) {
System.out.println(pDataTransfertObject.toString());
Artist artist = new Artist(pDataTransfertObject.getARTIST_ID());
if (artistValidator.isNotNullAndNotEmpty(pDataTransfertObject.getArtistFirstName())) {
artist.setArtistFirstName(pDataTransfertObject.getArtistFirstName());
}
if (artistValidator.isOptionPresent(pDataTransfertObject.getArtistLastName())) {
artist.setArtistLastName(pDataTransfertObject.getArtistLastName());
}
if (artistValidator.isOptionPresent(pDataTransfertObject.getArtistBiography())) {
artist.setArtistBiography(pDataTransfertObject.getArtistBiography());
}
if (artistValidator.isOptionPresent(pDataTransfertObject.getArtistBirthDate())) {
artist.setArtistBirthDate(pDataTransfertObject.getArtistBirthDate());
}
if (artistValidator.isOptionPresent(pDataTransfertObject.getArtistDeathDate())) {
artist.setArtistDeathDate(pDataTransfertObject.getArtistDeathDate());
}
return artist;
}
This is the Json I send to my controller :
{
"ARTIST_ID" : null,
"artistFirstName":"OH",
"artistLastName":"Test",
"artistBiography":"Je suis un test.",
"artistBirthDate":"1380-11-10",
"artistDeathDate": "1500-11-12",
"artistNationalities": [{
"nationality": {
"countryId" : 1
}
},
{"nationality":{
"countryId": 12
}
}
]
}
I also had to find a trick to save nationalities after artist because when I sent nationalities to database, jpa did not automatically add the saved artist to the nationality and the nationality was saved only with the country data, as you can see in the save method and the dto to do mapper.
I am sorry if it is not really clear, I do my best in english, thank you for understanding.
If your find and findAll is working then, you should be able to override return from save and use find jpa method using the artist id returned by save in your service layer that way you will have a complete json to return.
I want to allow to sort by every field in the class, without having to write switch/ if statements.
My idea was to find the Field that matches given string value by name and then, with Stream API neatly sort. IntelliJ screamed that i need to surround it with try-catch, so it is not so neatly looking, but that's not important, as it does not work.
private List<MyEntity> getSorted(List<MyEntity> list, SearchCriteria criteria) {
Field sortByField = findFieldInListByName(getFieldList(MyEntity.class), criteria.getSortBy());
return list.stream().sorted(Comparator.comparing(entity-> {
try {
return (MyEntity) sortByField.get(entity);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return entity;
})).collect(Collectors.toList());
}
In the MyEntity class I have added Comparable interface, but I am not sure what should be in the body of Compare(), as I dont want to specify how to compare objects, because it will change based on the selected sorting.
EDIT: Added Entity below:
#Data
#Entity
#Table(name = "role_management", schema = "mdr")
#Builder
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode
public class MyEntity implements Comparable{
#Id
#Column(name = "uuid", unique = true, insertable = false, updatable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID uuid;
#ManyToOne
#JoinColumn(name = "user_id", nullable = false)
private UserEntity user;
#Basic
#NonNull
#Column(name = "role")
private String role;
#Basic
#Column(name = "action")
#Enumerated(EnumType.STRING)
private RoleAction action;
#Basic
#Column(name = "goal")
#Enumerated(EnumType.STRING)
private RoleGoal goal;
#Column(name = "date")
private LocalDateTime date;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "reporter_id", referencedColumnName = "uuid")
private UserEntity reporter;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "authorizer_id", referencedColumnName = "uuid")
private UserEntity authorizer;
#Basic
#Column(name = "ezd")
private String ezd;
#Basic
#Column(name = "is_last")
private boolean isMostRecent;
#Override
public int compareTo(Object o) {
return 0;
}
}
EDIT 2: My code based on the #Sweeper solution:
UserEntity (nullable)
#Override
public int compareTo(UserEntity other) {
if (other == null) {
return 1;
}
return this.getMail().compareTo(other.getMail());
}
Comparator:
public static Comparator getSortComparator(Field sortByField) {
return Comparator.nullsLast(Comparator.comparing(entity -> {
try {
Object fieldValue = sortByField.get(entity);
if (!(fieldValue instanceof Comparable<?>) && fieldValue != null) {
throw new IllegalArgumentException("...");
}
return (Comparable) fieldValue;
} catch (IllegalAccessException e) {
throw new MdrCommonException(e.getLocalizedMessage());
}
}));
}
MyEntity should not implement Comparable. It is the fields, by which you are going to sort the list of MyEntity objects, that needs to be Comparable. For example, if you are sorting by the field user, which is a UserEntity, then UserEntity is the thing that needs to be comparable, not MyEntity.
The lambda's job should just be to check that the fields are indeed Comparable, and throw an exception if they are not.
Since you don't know the types of the fields at compile time, however, you'd have to use a raw type here. The comparing call would look like this:
Comparator.comparing(entity -> {
try {
Object fieldValue = sortByField.get(entity);
// This check still passes if the type of fieldValue implements Comparable<U>,
// where U is an unrelated type from the type of fieldValue, but this is the
// best we can do here, since we don't know the type of field at compile time
if (!(fieldValue instanceof Comparable<?>) && fieldValue != null) {
throw new IllegalArgumentException("Field is not comparable!");
}
return (Comparable)fieldValue;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
})
You can create automatically comparators for any field of any class using reflection but is better create specific comparators (will be typechecked).
Your entity is a normal class with normal fields then, the usual Java sorting machinery should do the job:
Basically, if you define one comparator for every field (even deep fields into your entity):
public final static Comparator<MyEntity> ByField1 = comparing(MyEntity::getField1);
public final static Comparator<MyEntity> ByField2 = comparing(MyEntity::getField2);
public final static Comparator<MyEntity> ByField3 = comparing(MyEntity::getField3);
public final static Comparator<MyEntity> ByDeep1 = comparing(a -> a.getField4().getDeep1());
public final static Comparator<MyEntity> ByDeep2 = comparing(a -> a.getField4().getDeep2());
public final static Comparator<MyEntity> ByDeep3 = comparing(a -> a.getField4().getDeep3());
You can sort using complex sorting expressions:
data.stream()
.sorted(ByField2.reversed().thenComparing(ByDeep2))
.forEach(System.out::println);
a full example could be
public static void main(String[] args) {
List<MyEntity> data =
Stream.of("Row1", "Row2").flatMap(field1 ->
Stream.of(101, 102).flatMap(field2 ->
Stream.of(true, false).flatMap(field3 ->
Stream.of("Row1", "Row2").flatMap(deep1 ->
Stream.of(101, 102).flatMap(deep2 ->
Stream.of(true, false).map(deep3 ->
new MyEntity(field1, field2, field3, new MyDeepField(deep1, deep2, deep3))))))))
.collect(toList());
data.stream()
.sorted(ByField2.reversed().thenComparing(ByDeep2))
.forEach(System.out::println);
}
#Getter
#Setter
#AllArgsConstructor
static class MyDeepField {
private String deep1;
private Integer deep2;
private Boolean deep3;
}
#Getter
#Setter
#AllArgsConstructor
static class MyEntity {
private String field1;
private Integer field2;
private Boolean field3;
private MyDeepField field4;
public final static Comparator<MyEntity> ByField1 = comparing(MyEntity::getField1);
public final static Comparator<MyEntity> ByField2 = comparing(MyEntity::getField2);
public final static Comparator<MyEntity> ByField3 = comparing(MyEntity::getField3);
public final static Comparator<MyEntity> ByDeep1 = comparing(a -> a.getField4().getDeep1());
public final static Comparator<MyEntity> ByDeep2 = comparing(a -> a.getField4().getDeep2());
public final static Comparator<MyEntity> ByDeep3 = comparing(a -> a.getField4().getDeep3());
#Override
public String toString() {
return "MyEntity{" +
"field1='" + field1 + '\'' +
", field2=" + field2 +
", field3=" + field3 +
", deep1=" + field4.getDeep1() +
", deep2=" + field4.getDeep2() +
", deep3=" + field4.getDeep3() +
'}';
}
}
with output
MyEntity{field1='Row1', field2=102, field3=true, deep1=Row1, deep2=101, deep3=true}
MyEntity{field1='Row1', field2=102, field3=true, deep1=Row1, deep2=101, deep3=false}
...
MyEntity{field1='Row2', field2=101, field3=false, deep1=Row2, deep2=102, deep3=true}
MyEntity{field1='Row2', field2=101, field3=false, deep1=Row2, deep2=102, deep3=false}
The criteria field into your SearchCriteria class is some field of type Comparator<MyEntity> or a mapping using an enumeration or parsing string expressions or so...
I am writing a PUT request API with spring and mongodb. But the save() inserts a new object instead of update the current one.
#Document("Test")
public class Expense {
#Field(name = "name")
private String expenseName;
#Field(name = "category")
private ExpenseCategory expenseCategory;
#Field(name = "amount")
private BigDecimal expenseAmount;
public Expense( String expenseName, ExpenseCategory expenseCategory, BigDecimal expenseAmount) {
this.expenseName = expenseName;
this.expenseCategory = expenseCategory;
this.expenseAmount = expenseAmount;
}
public String getExpenseName() {
return expenseName;
}
public void setExpenseName(String expenseName) {
this.expenseName = expenseName;
}
public ExpenseCategory getExpenseCategory() {
return expenseCategory;
}
public void setExpenseCategory(ExpenseCategory expenseCategory) {
this.expenseCategory = expenseCategory;
}
public BigDecimal getExpenseAmount() {
return expenseAmount;
}
public void setExpenseAmount(BigDecimal expenseAmount) {
this.expenseAmount = expenseAmount;
}
}
This is my reporsitory class
public interface ExpenseRepository extends MongoRepository<Expense, String> {
}
This is my Service class which shows how to update the class.
#Service
public class ExpenseService {
private final ExpenseRepository expenseRepository;
public ExpenseService(ExpenseRepository expenseRepository) {
this.expenseRepository = expenseRepository;
}
public void updateExpense(String id, Expense expense){
Expense savedExpense = expenseRepository.findById(id)
.orElseThrow(() -> new RuntimeException(
String.format("Cannot Find Expense by ID %s", id)));
savedExpense.setExpenseName(expense.getExpenseName());
savedExpense.setExpenseAmount(expense.getExpenseAmount());
savedExpense.setExpenseCategory(expense.getExpenseCategory());
expenseRepository.save(savedExpense);
}
}
This is my controller
#RestController
#RequestMapping("/api/expense")
public class ExpenseController {
private final ExpenseService expenseService;
public ExpenseController(ExpenseService expenseService) {
this.expenseService = expenseService;
}
#PutMapping("/{id}")
public ResponseEntity<Object> updateExpense(#PathVariable String id, #RequestBody Expense expense){
expenseService.updateExpense(id, expense);
return ResponseEntity.ok().build();
}
}
As shown in mongodb compass, mongodb auto generates an _id field for every object. So I do not define a id field or use #id annotation to define a primary for the collection. However, in the service class, expenseRepository.findById(id) retrieves the desired object and update it. Why does save() do the insert instead of update? Many thanks.
JPA Can't find the existing entry as no id field id set. You need to add an id field and set generation type to auto.
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
I'm creating eCommerce for merchants using spring boot with JPA.
I have an issue while creating the order service.
I want to only pass the ID of the nested objects in the request body instead of sending the full nest objects because the size will be extremely big.
Here is my code.
Merchant can do many orders
Order
#Entity
#Table(name = "Orders")
#XmlRootElement
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Order extends BasicModelWithIDInt {
#Basic(optional = false)
#Column(name = "Quantity")
private Integer quantity;
#Basic(optional = false)
#Size(min = 1, max = 150)
#Column(name = "Notes")
private String notes;
#JoinColumn(name = "ProductID", referencedColumnName = "ID")
#ManyToOne(optional = false, fetch = FetchType.LAZY)
#JsonIgnoreProperties
private Product productID;
#JoinColumn(name = "MerchantID", referencedColumnName = "ID")
#ManyToOne(optional = false, fetch = FetchType.LAZY)
private Merchent merchent;
#JoinColumn(name = "OrderSatusID", referencedColumnName = "ID")
#ManyToOne(optional = false, fetch = FetchType.EAGER)
private OrderStatus orderStatus;
// Getters and Setters
}
Order Holder
public class OrderHolder {
#NotNull
private Order order;
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
}
OrderRepo
public interface OrderRepo extends JpaRepository<Order, Integer> {
}
Order Controller
#RestController
#RequestMapping(value = "order", produces = MediaType.APPLICATION_JSON_VALUE)
public class OrderRestController extends BasicController<OrderHolder>{
#Autowired
private OrderRepo orderRepo;
#PostMapping("create")
public ResponseEntity<?> create(#RequestBody #Valid OrderHolder orderHolder, Principal principal) throws GeneralException {
log.debug( "create order {} requested", orderHolder.toString());
Order order = new Order();
order = orderHolder.getOrder();
System.out.println("###############"+order);
try {
order = orderRepo.save(order);
log.info( "Order {} has been created", order );
} catch (Exception e) {
log.error( "Error creating Order: ", e );
e.printStackTrace();
throw new GeneralException( Errors.ORDER_CREATION_FAILURE, e.toString() );
}
return ResponseEntity.ok( order );
}
}
I need request body to look like the below instead of including the full Merchant and Product objects inside the request.
You can make use of JsonView to return only id of product and merchant
public class OrderView {}
...
public class Product{
#Id
#JsonView(OrderView.class)
private Integer id
private String otherFieldWithoutJsonView
...
}
and then in your controller
#PostMapping("create")
#JsonView(OrderView.class) // this will return the product object with one field (id)
public ResponseEntity<?> create(#RequestBody #Valid OrderHolder orderHolder, Principal principal) throws GeneralException {
...
}
hope this can help you
Just have a separate contract class.
public class OrderContract {
private int merchantID;
private String notes;
....
//getter, setters
}
public class OrderHolder {
#NotNull
private OrderContract orderContract;
public OrderContract getOrderContract() {
return orderContract;
}
public void setOrder(OrderContract orderContract) {
this.orderContract = orderContract;
}
}
And before making a call to the Repository , translate from OrderContract to Order.
I would like to share something regarding this.
I have searched a lot on internet and tried lot of things, but the solution given here suited well for this scenario.
https://www.baeldung.com/jackson-deserialization
You need to create a Custom-deserializer for your model by extending StdDeserializer from com.fasterxml.jackson.databind.deser.std.StdDeserializer, where you just want to pass id's and not the whole object in the request.
I have given below example for User Model with Address object.
User(long userId, String name, Address addressId)
Address(long addressId, String wholeAddress)
Writing Deserializer for User class
public class UserDeserializer extends StdDeserializer<User> {
public User() {
this(null);
}
public User Deserializer(Class<?> vc) {
super(vc);
}
#Override
public User deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
JsonNode node = p.getCodec().readTree(p);
long id = 0;
long addressId = (Long) ((IntNode) node.get("addressId")).numberValue().longValue();
return new User(id, name, new Address(addressId, null)
}
Now you have to use
#JsonDeserialize(using = UserDeserializer.class)
public Class User {
...
}
POST request
Before custom deserialization
{
"name" : "Ravi",
"addressId" : { "id" : 1}
}
After custom Deserialization
{
"name" : "Ravi",
"addressId" : 1
}
Also while GET /user/:id call you will get the whole obj like
{
"name" : "Ravi",
"addressId" : { "id" : 1, "wholeAddress" : "Some address"}
}
I have an entity that gets "userType" during a query to the database.
public class OrderEntityXml {
#Id
#SequenceGenerator(name = "jpa.Sequence.t.order", sequenceName = "T_ORDER_SEQ", allocationSize = 1)
#GeneratedValue(generator = "jpa.Sequence.t.order", strategy = GenerationType.SEQUENCE)
private Long id;
private String customer;
#Type(type = "Order")
#Column(name = "order_xml")
private Order order;
public OrderEntityXml() {
}
There is an container for getting from an entity.
This container is using by Jackson.
This container also is using marshaller and unmarshaller
#XmlRootElement
#JacksonXmlRootElement(localName = "order")
public class Order implements Serializable {
private String customer;
#XmlElement(name = "orderItem")
#JacksonXmlProperty(localName = "orderItem")
#JacksonXmlElementWrapper(useWrapping = false)
private List<OrderItem> orderItems = new ArrayList<>();
public Order() {
}
public class OrderItem {
private String sku;
private Double price;
public OrderItem() {
}
I have set up a MapStruct.
CycleAvoidingMappingContext - ( This is necessary to avoid Cycling (and the appearance of stack overflow))
#Component
public class CycleAvoidingMappingContext {
private Map<Object, Object> knownInstances = new IdentityHashMap<Object, Object>();
#BeforeMapping
public <T> T getMappedInstance(Object source, #TargetType Class<T> targetType) {
T t = (T) knownInstances.get(source);
return t;
}
#BeforeMapping
public void storeMappedInstance(Object source, #MappingTarget Object target) {
knownInstances.put( source, target );
}
}
public interface CommonMapper<D, E> {
D toDto(E e, #Context CycleAvoidingMappingContext context);
E toEntity(D d, #Context CycleAvoidingMappingContext context);
Iterable<D> toListDto(Iterable<E> entityList);
Iterable<E> toListEntity(Iterable<D> dtoList);
}
MapperUtils - (This is utils for MapStruct)
public static Order convertToDto(OrderEntityXml orderEntityXml){
Order order = new Order();
String customer = orderEntityXml.getCustomer();
order.setCustomer(customer);
Order xmlOrder = orderEntityXml.getOrder();
List<OrderItem> orderItems = xmlOrder.getOrderItems();
order.setOrderItems(orderItems);
return order;
}
public static OrderEntityXml convertToEntity(Order order){
OrderEntityXml orderEntityXml = new OrderEntityXml();
String customer = order.getCustomer();
orderEntityXml.setCustomer(customer);
List<OrderItem> orderItems = order.getOrderItems();
Order orderInEntity = new Order();
orderInEntity.setOrderItems(orderItems);
orderInEntity.setCustomer(customer);
orderEntityXml.setOrder(orderInEntity);
return orderEntityXml;
}
OrderDtoMapper - It is basic interface for me, for classes generation
#Mapper(componentModel = "spring")
public interface OrderDtoMapper extends CommonMapper<Order, OrderEntityXml> {
#Override
default Order toDto(OrderEntityXml orderEntityXml, CycleAvoidingMappingContext context) {
return convertToDto(orderEntityXml);
}
#Override
default OrderEntityXml toEntity(Order order, CycleAvoidingMappingContext context) {
return convertToEntity(order);
}
#Override
default Iterable<Order> toListDto(Iterable<OrderEntityXml> entityList) {
Iterable<Order> collect = StreamSupport.stream(entityList.spliterator(), false)
.map(MapperUtils::convertToDto)
.collect(Collectors.toList());
return collect;
}
}
For each data type, you will have to make your own utilities, and this is cumbersome.
Сan I remove extra code and configure it via MapStruct interfaces ?
I suspect I've complicated the code.
Сan anyone tell you what needs to be tweaked or what technology can be applied to mapStruct to make the code unified
Looking at the posted entities I don't see why you need to use CycleAvoidingMappingContext. There is no cyclic dependency between your objects.
In order to avoid the manual code you've written you can use the MapStruct #Mapping annotation to customize how certain fields need to be mapped.
So in your case it would be something like:
#Mapper(componentModel = "spring")
public interface OrderDtoMapper extends CommonMapper<Order, OrderEntityXml> {
#Override
#Mapping(target = "orderItems", source = "order.orderItems")
Order toDto(OrderEntityXml orderEntityXml, #Context CycleAvoidingMappingContext context);
#Override
#Mapping(target = "order", source = "order")
OrderEntityXml toEntity(Order order, CycleAvoidingMappingContext context);
Order cloneOrder(Order order);
}
The only customizations you need are the following:
For toDto to tell MapStruct that when you are mapping from OrderEntityXml to Order you want to map the order.orderItems into the orderItems.
For toEntity to tell MapStruct that you when you are mapping from Order to OrderEntityXml you want to map the method order parameters into the order of the `OrderEntityXml
Additionally we add Order cloneOrder(Order) so that MapStruct creates a new object when mapping between Order otherwise the same object will be used.
The customer in both cases will be automatically mapped, since it matches on both sides.
There is no need to provide custom method for the Iterable mapping because MapStruct will do that automatically for you. It knows