Spring Jackson deserialize object with reference to existing object by Id - java

I have three entity classes of the following:
Shipments Entity:
#Entity
#Table(name = "SHIPMENT")
public class Shipment implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "SHIPMENT_ID", nullable = false)
private int shipmentId;
#Column(name = "DESTINATION", nullable = false)
private String destination;
#OneToMany(mappedBy = "shipment")
private List<ShipmentDetail> shipmentDetailList;
//bunch of other variables omitted
public Shipment(String destination) {
this.destination = destination;
shipmentDetailList = new ArrayList<>();
}
Shipment Details Entity:
#Entity
#Table(name = "SHIPMENT_DETAIL")
public class ShipmentDetail implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "SHIPMENT_DETAIL_ID", nullable = false)
private int shipmentDetailId;
#ManyToOne
#JoinColumn(name = "PRODUCT_ID", nullable = false)
private Product product;
#JsonIgnore
#ManyToOne
#JoinColumn(name = "SHIPMENT_ID", nullable = false)
private Shipment shipment;
//bunch of other variables omitted
public ShipmentDetail() {
}
public ShipmentDetail(Shipment shipment, Product product) {
this.product = product;
this.shipment = shipment;
}
Product Entity:
#Entity
#Table(name = "Product")
public class Product implements Serializable {
#Id
#Column(name = "PRODUCT_ID", nullable = false)
private String productId;
#Column(name = "PRODUCT_NAME", nullable = false)
private String productName;
//bunch of other variables omitted
public Product() {
}
public Product(String productId, String productName) {
this.productId = productId;
this.productName = productName;
}
I am receiving JSONs through a rest API. The problem is I do not know how to deserialize a new Shipment with shipmentDetails that have relationships to already existing objects just by ID. I know you can simply deserialize with the objectmapper, but that requires all the fields of product to be in each shipmentDetail. How do i instantiate with just the productID?
Sample JSON received
{
"destination": "sample Dest",
"shipmentDetails": [
{
"productId": "F111111111111111"
},
{
"productId": "F222222222222222"
}
]
}
Currently my rest endpoint would then receive the JSON, and do this:
public ResponseEntity<String> test(#RequestBody String jsonString) throws JsonProcessingException {
JsonNode node = objectMapper.readTree(jsonString);
String destination = node.get("destination").asText();
Shipment newShipment = new Shipment(destination);
shipmentRepository.save(newShipment);
JsonNode shipmentDetailsArray = node.get("shipmentDetails");
int shipmentDetailsArrayLength = shipmentDetailsArray.size();
for (int c = 0; c < shipmentDetailsArrayLength; c++) {
String productId = node.get("productId").asText();
Product product = productRepository.findById(productId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No product with ID of: " + productId + " exists!"));
ShipmentDetail shipmentDetail = new ShipmentDetail(newShipment, product, quantity);
shipmentDetailRepository.save(shipmentDetail);
}
}
what i want to do is:
public ResponseEntity<String> test2(#RequestBody String jsonString) throws JsonProcessingException {
JsonNode wholeJson = objectMapper.readTree(jsonString);
Shipment newShipment = objectMapper.treeToValue(wholeJson, Shipment.class);
return new ResponseEntity<>("Transfer Shipment successfully created", HttpStatus.OK);
}
I tried this solution to no. avail:
Deserialize with Jackson with reference to an existing object
How do I make the product entity search for an existing product instead of trying to create a new product. The hacky extremely inefficient workaround I have been using is to traverse the json array, and for every productId find the product using the productRepository, and then set the shipmentDetail with the product one by one. Im not sure if this is best practice as im self learning spring.
So in pseudocode what im trying to do would be:
Receive JSON
Instantiate Shipment entity
Instantiate an array of shipmentDetail entities
For each shipmentDetail:
1. Find product with given productId
2. Instantiate shipmentDetail with product and shipment
Code has been significantly simplified to better showcase the problem,

You have a bottleneck in your code in this part:
Product product = productRepository.findById(productId)
Because you are making a query for each productId, and it will perform badly with large number of products. Ignoring that, I will recommend this aproach.
Build your own deserializer (see this):
public class ShipmentDeserializer extends JsonDeserializer {
#Override
public Shipment deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
String destination = node.get("destination").asText();
Shipment shipment = new Shipment(destination);
JsonNode shipmentDetailsNode = node.get("shipmentDetails");
List shipmentDetailList = new ArrayList();
for (int c = 0; c < shipmentDetailsNode.size(); c++) {
JsonNode productNode = shipmentDetailsNode.get(c);
String productId = productNode.get("productId").asText();
Product product = new Product(productId);
ShipmentDetail shipmentDetail = new ShipmentDetail(product);
shipmentDetailList.add(shipmentDetail);
}
shipment.setShipmentDetailList(shipmentDetailList);
return shipment;
}
}
Add the deserializer to your Shipment class:
#JsonDeserialize(using = ShipmentDeserializer .class)
public class Shipment {
// Class code
}
Deserialize the string:
public ResponseEntity test2(#RequestBody String jsonString) throws JsonProcessingException {
Shipment newShipment = objectMapper.readValue(jsonString, Shipment.class);
/* More code */
return new ResponseEntity("Transfer Shipment successfully created", HttpStatus.OK);
}
At this point, you are only converting the Json into classes, so we need to persist the data.
public ResponseEntity test2(#RequestBody String jsonString) throws JsonProcessingException {
Shipment newShipment = objectMapper.readValue(jsonString, Shipment.class);
shipmentRepository.save(newShipment);
List<ShipmentDetail> shipmentDetails = newShipment.getShipmentDetailList();
for (int i = 0; i < shipmentDetails.size(); c++) {
ShipmentDetail shipmentDetail = shipmentDetails.get(i);
shipmentDetail.setShipment(newShipment);
Product product = productRepository.findById(productId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No product with ID of: " + productId + " exists!"));
shipmentDetail.setProduct(product);
shipmentDetailRepository.save(shipmentDetail);
}
return new ResponseEntity("Transfer Shipment successfully created", HttpStatus.OK);
}
I know you want to reduce the code in the test method, but I DO NOT RECOMMEND to combine the Json deserialize with the persistence layer. But if you want to follow that path, you could move the productRepository.findById(productId) into the ShipmentDeserializer class like this:
public class ShipmentDeserializer extends JsonDeserializer {
#Override
public Shipment deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
String destination = node.get("destination").asText();
Shipment shipment = new Shipment(destination);
JsonNode shipmentDetailsNode = node.get("shipmentDetails");
List shipmentDetailList = new ArrayList();
for (int c = 0; c < shipmentDetailsNode.size(); c++) {
JsonNode productNode = shipmentDetailsNode.get(c);
String productId = productNode.get("productId").asText();
Product product = productRepository.findById(productId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No product with ID of: " + productId + " exists!"));
ShipmentDetail shipmentDetail = new ShipmentDetail(product);
shipmentDetailList.add(shipmentDetail);
}
shipment.setShipmentDetailList(shipmentDetailList);
return shipment;
}
}
But if you want to do that, you need to inject the repository into the deserializer (see this).

I think your current approach is not a bad solution, you are dealing with the problem correctly and in few steps.
Any way, one thing you can try is the following.
The idea will be to provide a new field, productId, defined on the same database column that supports the relationship with the Product entity, something like:
#Entity
#Table(name = "SHIPMENT_DETAIL")
public class ShipmentDetail implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "SHIPMENT_DETAIL_ID", nullable = false)
private int shipmentDetailId;
#Column(name = "PRODUCT_ID", nullable = false)
private String productId;
#ManyToOne
#JoinColumn(name = "PRODUCT_ID", insertable = false, updatable = false)
private Product product;
#JsonIgnore
#ManyToOne
#JoinColumn(name = "SHIPMENT_ID", nullable = false)
private Shipment shipment;
//bunch of other variables omitted
public ShipmentDetail() {
}
public ShipmentDetail(Shipment shipment, Product product) {
this.product = product;
this.shipment = shipment;
}
}
The product field must be annotated as not insertable and not updatable: on the contrary, Hibernate will complaint about which field should be used to maintain the relationship with the Product entity, in other words, to maintain the actual column value.
Modify the Shipment relationship with ShipmentDetail as well to propagate persistence operations (adjust the code as per your needs):
#OneToMany(mappedBy = "shipment", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ShipmentDetail> shipmentDetailList;
Then, you can safely rely on the Spring+Jackson deserialization and obtain a reference to the received Shipment object:
public ResponseEntity<String> processShipment(#RequestBody Shipment shipment) {
// At this point shipment should contain the different details,
// each with the corresponding productId information
// Perform the validations required, log information, if necessary
// Save the object: it should persist the whole object tree in the database
shipmentRepository.save(shipment);
}
This approach has an obvious drawback, the existence of the Product is not checked beforehand.
Although you can ensure data integrity at database level with the use of foreign keys, perhaps it would be convenient to validate that the information is right before perform the actual insertion:
public ResponseEntity<String> processShipment(#RequestBody Shipment shipment) {
// At this point shipment should contain the different details,
// each with the corresponding productId information
// Perform the validations required, log information, if necessary
List<ShipmentDetail> shipmentDetails = shipment.getShipmentDetails();
if (shipmentDetails == null || shipmentDetails.isEmpty()) {
// handle error as appropriate
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No shipment details provided");
}
shipmentDetails.forEach(shipmentDetail -> {
String productId = shipmentDetail.getProductId();
Product product = productRepository.findById(productId).orElseThrow(
() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"No product with ID of: " + productId + " exists!")
)
});
// Everything looks fine, save the object now
shipmentRepository.save(shipment);
}

Related

Map List < Object > to List < ObjectSrv >

I want to Map List < City > to List < CitySrv > in the service layer but i dont have any idea how to do that.
the City srv that i want to Map: (srv = server response value)
#Getter
#Setter
#SuperBuilder
#NoArgsConstructor
#AllArgsConstructor
public class CitySrv {
private Long id;
private String name;
private Long state;
}
the City Entity that want to map From:
#AllArgsConstructor
#NoArgsConstructor
#Entity
#SuperBuilder
#Getter
#Setter
#Table(name = "IOTP_CITY")
public class City extends BaseEntity {
#Column(name = "NAME")
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "state_id")
private State state;
#OneToMany(mappedBy = "city")
private List<Branch> branches = new ArrayList<Branch>();
}
i used Model mapper before but for some reason i cant use it anymore.
#Override
public GenericResponse<List<CitySrv>> getAllCities() throws ProjectException {
Optional<List<City>> citiiesSrvResponse = cityRepository.getAllCities();
if (citiiesSrvResponse.isPresent()) {
List<CitySrv> citiesList = modelMapper.map(citiiesSrvResponse.get(), new
TypeToken<List<CitySrv>>() {
}.getType());
return ResponseUtil.getResponse(citiesList);
} else throw new ProjectException(ExceptionStatus.NOT_FOUND);
}
i handle it just like this but its not so good
public GenericResponse<List<CitySrv>> getAllCities() throws ProjectException
{
Optional<List<City>> citiiesSrvResponse = cityRepository.getAllCities();
CitySrv citySrv = new CitySrv();
List<CitySrv> citySrvList = new ArrayList<>();
if (citiiesSrvResponse.isPresent()) {
for (City eachCity : citiiesSrvResponse.get()) {
citySrv.setId(eachCity.getId());
citySrv.setName(eachCity.getName());
citySrv.setState(eachCity.getState().getId());
citySrvList.add(citySrv);
}
return ResponseUtil.getResponse(citySrvList);
} else throw new ProjectException(ExceptionStatus.NOT_FOUND);
}
For modelMapper to work your source and destination have to be similar enough. You want to map the entity State in City to a Long which is its id in CitySrv. Which I would guess doesn't fulfill this criteria. You have to tell modelMapper how to map that by configuring a mapping
TypeMap<City, CitySrv> propertyMapper = modelMapper.createTypeMap(City.class, CitySrv.class);
propertyMapper.addMappings(
mapper -> mapper.map(src -> src.getState().getId, CitySrv::setState)
);
Best you do that in a global config.
You can find more info in this baeldung article

Reuse an existing id if exist in the database

I would like to do the following:
Inserting the CityHistory into the database using JPA.
The first time there is no data, so a new city will be inserted. (IT WORKS FINE)
the (IDENTIFICATION) within the city table is a unique field.
What I want to achieve is when I am inserting the same city again is to reuse the existing field instead of trying to create a new one (identification will be like a city's unique name).
So how can I do that using JPA or Hibernate?
#Entity
public class CityHistory extends History implements Serializable {
#Id
#Column(name = "KEY_CITY_HISTORY", nullable = false, precision = 19)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "CITY_ID", nullable = false, foreignKey = #ForeignKey(name = "FK_CITY_ID"))
private City cityId;
#Column(name = "CITY_NAME", nullable = false)
private String cityName;
}
#Entity
public class City implements Serializable {
#Id
#Column(name = "KEY_CITY", nullable = false, precision = 19)
private Long id;
#Column(name = "IDENTIFICATION", nullable = false, unique = true)
private String identification;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "MUNICIPALITY_ID", foreignKey = #ForeignKey(name = "FK_MUNICIPALITY_ID"))
private Municipality municipalityId;
}
UPDATE
Here is how I am writing the data to the database,
It's a Spring Batch itemWriter
#Component
public class InfoItemWriter implements ItemWriter<Object> {
#Autowired
private CityHistoryRepository cityHistoryRepository;
#Override
public void write(List<? extends Object> items) throws Exception {
if (items.size() > 0 && items.get(0) instanceof CityHistory) {
cityHistoryRepository.saveAll((List<? extends CityHistory>) items);
}
}
}
First of all thanks to all who tried to help!
Reading the resources that #Benjamin Maurer provided:
I don't think you want the cascade on the ManyToOne side, see One-To-Many
The most common Parent – Child association consists of a one-to-many and a many-to-one relationship, where the cascade being useful for the one-to-many side only
As the relation I have is ManyToOne it was really not useful to use the cascade and doesn't serve my need.
I used a different approache to reach the goal. I have created a service where it validates the existence of a city, then adds a new city if it does not exist.
#Service
public class CityHistoryServiceImpl implements CityHistoryService {
#Autowired
CityRepository cityRepository;
#Autowired
CityHistoryRepository cityHistoryRepository;
#Override
public Optional<CityHistory> addCityHistory(City city, String cityName, ..) {
if (city != null && cityName != null) {
City city1 = addCityIfNotExist(city);
CityHistory cityHistory = new CityHistory();
cityHistory.setCityId(city1);
cityHistory.setCityName(cityName);
cityHistoryRepository.save(cityHistory);
return Optional.of(cityHistory);
}
return Optional.empty();
} ....
private City addCityIfNotExist(City city) {
City city1 = cityRepository.findFirstByBagId(city.getBagId());
if (city1 == null) {
city1 = cityRepository.save(city);
}
return city1;
}
}
Hibernate will use the #Id property of City to determine if it is new or not. When it is null, Hibernate couldn't possibly know that a similar entry already exists.
So you need to perform a query to find each city first:
for (var history : histories) {
var cities = em.createQuery("select city from City city where city.identification = ?1", City.class)
.setParameter(1, history.getCityId().getIdentification())
.getResultList();
if (!cities.isEmpty()) {
history.setCityId(cities.get(0));
}
em.persist(history);
}
If you use Hibernate and City.identification is unique and always non-null, you can use it as a NaturalID:
In City:
#NaturalId
private String identification;
Then:
for (var history : histories) {
var city = em.unwrap(Session.class)
.byNaturalId(City.class)
.using("identification", history.getCityId().getIdentification())
.getReference();
if (city != null) {
history.setCityId(city);
}
em.persist(history);
}
But if you do have City.id set, i.e., not null, you can use EntityManager.merge to get a managed entity:
for (var history : histories) {
City city = history.getCityId();
if (city.getId() != null) {
city = em.merge(city);
history.setCityId(city);
}
em.persist(history);
}
One more remark: We are not in the relational domain, but we are mapping object graphs. So calling your fields cityId and municipalityId is arguably wrong - even the type says so: City cityId.
They are not just plain identifiers, but full fledged objects: City city.

Jackson ObjectMapper Hibernate issue

I am working on a Spring Boot 2.0 / Java 8 shopping cart online application. I use Hibernate as the ORM framework.
I have two entities, Order and OrderDetail shown below:
#Entity
#Table(name = "orders")
public class Order extends AbstractEntityUuid {
#Column(name = "order_number", unique = true)
private String orderNumber;
#JsonBackReference
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id", nullable = false)
private User user;
#Column(name = "total_amount")
private BigDecimal totalAmount = BigDecimal.ZERO;
#CreatedDate
#Column(name = "created_on", columnDefinition = "DATETIME", updatable = false)
protected LocalDateTime created;
#OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JsonManagedReference
private Set<OrderDetail> items = new HashSet<>();
#OneToOne(fetch = FetchType.LAZY, orphanRemoval = true)
#JoinColumn(nullable = false, name = "card_details_id")
private CardDetails card;
#OneToOne(fetch = FetchType.LAZY, orphanRemoval = true)
#JoinColumn(nullable = false, name = "shipping_address_id")
private Address shippingAddress;
#OneToOne(fetch = FetchType.LAZY, orphanRemoval = true)
#JoinColumn(name = "billing_address_id")
private Address billingAddress;
//getters and setters
}
#Entity
#Table(name = "order_detail")
public class OrderDetail extends AbstractPersistable<Long> {
#Column(name = "quantity")
private Integer quantity;
#Column(name = "total_amount")
private BigDecimal totalAmount = BigDecimal.ZERO;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "order_id", nullable = false)
#JsonBackReference
private Order order;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "product_id", nullable = false)
private Product product;
//getters and setters
}
When the user heads over to his Orders, he should be able to see information related only to the order itself (no details).
For that reason, I retrieve data only from the order table. Following is my repository:
public interface OrderRepository extends CrudRepository<Order, Long> {
#Query("FROM Order o WHERE o.user.email = ?1")
List<Order> findOrdersByUser(String email);
}
In the service, what I do is simply calling the above method and converting to a the dto counterpart.
#Transactional(readOnly = true)
public List<OrdersPreviewDTO> getOrdersPreview(String email) {
List<Order> orders = orderRepository.findOrdersByUser(email);
return orderConverter.convertToOrderPreviewDTOs(orders);
}
The converter uses an Jackson ObjectMapper object under the hood.
List<OrdersPreviewDTO> convertToOrderPreviewDTOs(List<Order> orders) {
return orders.stream()
.map(o -> objectMapper.convertValue(o, OrdersPreviewDTO.class))
.collect(toList());
}
The objectMapper is inject by Spring and defined in a configuration class:
#Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();
return objectMapper;
}
The OrdersPreviewDTO dto object contains just a subset of the Order entity, because as I already mentioned, in the Orders page I want to show only high level properties of the user's orders, and nothing related to their details.
#JsonIgnoreProperties(ignoreUnknown = true)
public class OrdersPreviewDTO {
private String orderNumber;
#JsonFormat(pattern = "dd/MM/yyyy HH:mm")
private LocalDateTime created;
private BigDecimal totalAmount;
#JsonCreator
public OrdersPreviewDTO(
#JsonProperty("orderNumber") String orderNumber,
#JsonProperty("created") LocalDateTime created,
#JsonProperty("totalAmount") BigDecimal totalAmount) {
this.orderNumber = orderNumber;
this.created = created;
this.totalAmount = totalAmount;
}
//getters and setters
}
Everything works fine, the order entity is converted automagically by Jackson into its dto counterpart.
The problem come out when looking at the query executed by Hibernate under the hood.
Hibernate unwrap the collection of order details for each order and execute a query to retrieve data from each child collection:
select carddetail0_.id as id1_2_0_, carddetail0_.brand as brand2_2_0_, carddetail0_.created as created3_2_0_, carddetail0_.exp_month as exp_mont4_2_0_, carddetail0_.exp_year as exp_year5_2_0_, carddetail0_.last4 as last6_2_0_ from card_details carddetail0_ where carddetail0_.id=?
select address0_.id as id1_1_0_, address0_.created as created2_1_0_, address0_.last_modified as last_mod3_1_0_, address0_.city as city4_1_0_, address0_.country as country5_1_0_, address0_.first_name as first_na6_1_0_, address0_.last_name as last_nam7_1_0_, address0_.postal_code as postal_c8_1_0_, address0_.state as state9_1_0_, address0_.street_address as street_10_1_0_, address0_.telephone as telepho11_1_0_ from address address0_ where address0_.id=?
select items0_.order_id as order_id4_4_0_, items0_.id as id1_4_0_, items0_.id as id1_4_1_, items0_.order_id as order_id4_4_1_, items0_.product_id as product_5_4_1_, items0_.quantity as quantity2_4_1_, items0_.total_amount as total_am3_4_1_ from order_detail items0_ where items0_.order_id=?
and tons of others more.
Whether I modify the code in the following way, Hibernate runs only the expected query on the Order table:
This line of code:
objectMapper.convertValue(o, OrdersPreviewDTO.class)
is replaced by the following dirty fix:
new OrdersPreviewDTO(o.getOrderNumber(), o.getCreated(), o.getTotalAmount())
Query run by Hibernate:
select order0_.id as id1_5_, order0_.billing_address_id as billing_6_5_, order0_.card_details_id as card_det7_5_, order0_.created_on as created_2_5_, order0_.one_address as one_addr3_5_, order0_.order_number as order_nu4_5_, order0_.shipping_address_id as shipping8_5_, order0_.total_amount as total_am5_5_, order0_.user_id as user_id9_5_
from orders order0_ cross join user user1_
where order0_.user_id=user1_.id and user1_.user_email=?
My question is. Is there a way to tell Jackson to map only the Dtos field so that it doesn't trigger lazy loading fetches through Hibernate for the non required fields?
Thank you
The short answer is no, don't try and be so clever. Manually create your DTO to control any lazy loading, then use Jackson on the DTO outside the transaction.
The Long answer is yes, you can override MappingJackson2HttpMessageConverter and control which fields get called from the entity.
#Configuration
public class MixInWebConfig extends WebMvcConfigurationSupport {
#Bean
public MappingJackson2HttpMessageConverter customJackson2HttpMessageConverter2() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.addMixIn(DTO1.class, FooMixIn.class);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
jsonConverter.setObjectMapper(objectMapper);
return jsonConverter;
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(customJackson2HttpMessageConverter2());
}
}
Then
#Override
protected void processViews(SerializationConfig config, BeanSerializerBuilder builder) {
super.processViews(config, builder);
if (classes.contains(builder.getBeanDescription().getBeanClass())) {
List<BeanPropertyWriter> originalWriters = builder.getProperties();
List<BeanPropertyWriter> writers = new ArrayList<BeanPropertyWriter>();
for (BeanPropertyWriter writer : originalWriters) {
String propName = writer.getName();
if (!fieldsToIgnore.contains(propName)) {
writers.add(writer);
}
}
builder.setProperties(writers);
}
}
}
here is a working example.
+1 for Essex Boy answer. I just want to add that you can directly return DTO from your JPQL Queries instead of using Jackson. It avoids a transformation from the database to your object Order and then another transformation from Order object to OrdersPreviewDTO object.
For example, you need to change your query in your repository to do it. It would be something like :
public interface OrderRepository extends CrudRepository<Order, Long> {
#Query("SELECT new OrdersPreviewDTO(o.order_number, o.created_on, o.total_amount)) FROM Order o WHERE o.user.email = ?1")
List<OrdersPreviewDTO> findOrdersByUser(String email);
}
If OrdersPreviewDTO is strictly a subset of your Order class, why not simply use the #JsonView annotation to automatically create a simple view in your controller? See https://spring.io/blog/2014/12/02/latest-jackson-integration-improvements-in-spring for example.
In case you need a DTO for both input and output, also consider using http://mapstruct.org/

Jackson JSON serializer returns extra collection name

I have an list of things that I return as a jackson list. What I would like is this:
"things": { [
{
"id": "1234",
..list of students
but currently, I am getting this:
"things": {
"HashSet": [
{
"id": "1234",
I am using a JsonSerializer>, which is why it adds the HashSet field. I tried adding a json property on the field, but its not allowed since it is a local variable.
I am currently using the jackson library and have looked into:
Jackson annotation - How to rename element names?
How to rename root key in JSON serialization with Jackson
but they seem to have a different issue altogether. Any ideas? Thanks!
Edit:
Added the class implementations. Note that I call owner, that contains Things. Also, my jpa annotations are there as well. Thanks.
#Entity #Table(name = "owner")
public class Owner extends BaseEntity implements Persistence {
#Column
private String name;
#Column
private String description;
#Column
private Integer capacity;
#Column
#JsonSerialize(using = ThingSerializer.class)
#JsonProperty("things")
#ManyToMany(fetch = FetchType.EAGER, mappedBy = "owners")
private Set<Thing> tihngs = new HashSet<>();
public class ThingSerializer extends JsonSerializer<Set<Thing>> {
#Override public void serialize(Set<Thing> thingSet, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws
IOException,
JsonProcessingException {
Set<Thing> things = new HashSet<>();
thingSet.forEach(thing -> {
Thing t = new Thing();
t.setCreatedBy(thing.getCreatedBy());
t.setCreationDate(thing.getCreationDate());
t.setId(thing.getId());
t.setDateModified(thing.getDateModified());
t.setModifiedBy(thing.getModifiedBy());
t.setStatus(thing.getStatus());
things.add(s);
});
jsonGenerator.writeObject(things);
}
}
Thing Entity
#Entity
#Table(name = "thing")
public class Thing extends BaseEntity implements Persistence {
private static final long serialVersionUID = 721739537425505668L;
private String createdBy;
private Date creationDate;
.
.
.
#OneToMany(fetch = FetchType.EAGER, cascade = { CascadeType.PERSIST, CascadeType.MERGE })
#JoinTable(name = "ThingOwner", joinColumns = #JoinColumn(name = "thing_id") , inverseJoinColumns = #JoinColumn(name = "owner_id") )
private Set<Owner> owners = new HashSet<>();
Why don't you use ObjectMapper to serialize and deserialize your data?
Have a look at this small test, i believe it does what you want:
#Test
public void myTest() throws Exception{
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(
mapper.getSerializationConfig().
getDefaultVisibilityChecker().
withFieldVisibility(JsonAutoDetect.Visibility.ANY).
withGetterVisibility(JsonAutoDetect.Visibility.NONE).
withSetterVisibility(JsonAutoDetect.Visibility.NONE).
withCreatorVisibility(JsonAutoDetect.Visibility.NONE).
withIsGetterVisibility(JsonAutoDetect.Visibility.NONE));
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
mapper.setSerializationInclusion(Include.NON_NULL);
TestObject value = new TestObject();
value.a = "TestAvalue";
value.set = new HashSet<>();
value.set.add(new SetValueObject(1, 1));
value.set.add(new SetValueObject(2, 2));
String test = mapper.writeValueAsString(value);
System.out.println(test);
}
public class TestObject{
String a;
Set<SetValueObject> set;
}
public class SetValueObject{
public SetValueObject(int a, int b){
this.a = a;
this.b = b;
}
int a;
int b;
}
Output:
{"a":TestAvalue","set":[{"a":1,"b":1},{"a":2,"b":2}]}
Tested with Jackson 2.6.1
And i have modified one of my tests, so i'am not sure that you need all of this ObjectMapper config => it's just to give you an idea of another approach by using ObjectMapper.

Spring-MVC, Hibernate : Creating DTO objects from Domain objects

I am working on a Spring-MVC application in which I am trying to search for List of GroupNotes in database. The mapping in my project is GroupCanvas has one-to-many mapping with GroupSection and GroupSection has one-to-many mapping with GroupNotes. Because of these mappings, I was getting LazyInitializationException. As suggested on SO, I should be converting the objects to a DTO objects for transfer. I checked on net, but couldnt find a suitable way to translate those.
I have just created a new List to avoid the error, but one field is still giving me an error. I would appreciate if anyone tells me either how to fix this error or convert the objects to a DTO objects so they can be transferred.
Controller code :
#RequestMapping(value = "/findgroupnotes/{days}/{canvasid}")
public #ResponseBody List<GroupNotes> findGroupNotesByDays(#PathVariable("days")int days, #PathVariable("canvasid")int canvasid){
List<GroupNotes> groupNotesList = this.groupNotesService.findGroupNotesByDays(days,canvasid);
List<GroupNotes> toSendList = new ArrayList<>();
for(GroupNotes groupNotes : groupNotesList){
GroupNotes toSendNotes = new GroupNotes();
toSendNotes.setMnotecolor(groupNotes.getMnotecolor());
toSendNotes.setNoteCreationTime(groupNotes.getNoteCreationTime());
toSendNotes.setMnotetag(groupNotes.getMnotetag());
toSendNotes.setMnotetext(groupNotes.getMnotetext());
toSendNotes.setAttachCount(groupNotes.getAttachCount());
toSendNotes.setNoteDate(groupNotes.getNoteDate());
toSendList.add(toSendNotes);
}
return toSendList;
}
GroupNotesDAOImpl :
#Override
public List<GroupNotes> searchNotesByDays(int days, int mcanvasid) {
Session session = this.sessionFactory.getCurrentSession();
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_YEAR, -days);
long daysAgo = cal.getTimeInMillis();
Timestamp nowMinusDaysAsTimestamp = new Timestamp(daysAgo);
GroupCanvas groupCanvas = (GroupCanvas) session.get(GroupCanvas.class,mcanvasid);
Query query = session.createQuery("from GroupSection as n where n.currentcanvas.mcanvasid=:mcanvasid");
query.setParameter("mcanvasid", mcanvasid);
List<GroupSection> sectionList = query.list();
List<GroupNotes> notesList = new ArrayList<GroupNotes>();
for (GroupSection e : sectionList) {
System.out.println("Section name is "+e.getMsectionname());
GroupSection groupSection = (GroupSection) session.get(GroupSection.class,e.getMsectionid());
Query query1 = session.createQuery("from GroupNotes as gn where gn.ownednotes.msectionid=:msectionid and gn.noteCreationTime >:limit");
query1.setParameter("limit", nowMinusDaysAsTimestamp);
query1.setParameter("msectionid",e.getMsectionid());
notesList.addAll(query1.list());
}
// I am getting the data below, but I get JSON errors.
for(GroupNotes groupNotes : notesList){
System.out.println("Group notes found are "+groupNotes.getMnotetext());
}
return notesList;
}
GroupCanvas model :
#Entity
#Table(name = "membercanvas")
public class GroupCanvas{
#OneToMany(mappedBy = "currentcanvas",fetch=FetchType.LAZY, cascade = CascadeType.REMOVE)
#JsonIgnore
private Set<GroupSection> ownedsection = new HashSet<>();
#JsonIgnore
public Set<GroupSection> getOwnedsection() {
return this.ownedsection;
}
public void setOwnedsection(Set<GroupSection> ownedsection) {
this.ownedsection = ownedsection;
}
}
GroupSection model :
#Entity
#Table(name = "membersection")
public class GroupSection{
#OneToMany(mappedBy = "ownednotes", fetch = FetchType.EAGER,cascade = CascadeType.REMOVE)
#JsonIgnore
private Set<GroupNotes> sectionsnotes = new HashSet<>();
public Set<GroupNotes> getSectionsnotes(){
return this.sectionsnotes;
}
public void setSectionsnotes(Set<GroupNotes> sectionsnotes){
this.sectionsnotes=sectionsnotes;
}
}
GroupNotes model :
#Entity
#Table(name="groupnotes")
public class GroupNotes{
#Id
#Column(name="mnoteid")
#GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "mnote_gen")
#SequenceGenerator(name = "mnote_gen",sequenceName = "mnote_seq")
#org.hibernate.annotations.Index(name = "mnoticesidindex")
private int mnoticesid;
#Column(name = "mnotetext")
private String mnotetext;
#Column(name = "mnoteheadline")
private String mnotetag;
#Column(name = "mnotecolor")
private String mnotecolor;
#Column(name = "mnoteorder")
private double mnoteorder;
#Column(name = "attachmentcount")
private int attachCount;
#Column(name = "notedate")
private String noteDate;
#Column(name = "uploader")
private String uploader;
#Column(name = "activeedit")
private boolean activeEdit;
#Column(name = "notedisabled")
private boolean noteDisabled;
#Column(name = "noteinactive")
private boolean noteInActive;
#Column(name = "notecreatoremail")
private String noteCreatorEmail;
#Column(name = "prefix")
private String prefix;
#Column(name = "timestamp")
private Timestamp noteCreationTime;
#Transient
private boolean notRead;
#Transient
private String tempNote;
#Transient
private String canvasUrl;
#ManyToOne
#JoinColumn(name = "msectionid")
#JsonIgnore
private GroupSection ownednotes;
#JsonIgnore
public GroupSection getOwnednotes(){return this.ownednotes;}
public void setOwnednotes(GroupSection ownednotes){this.ownednotes=ownednotes;}
#JsonIgnore
public int getOwnedSectionId(){
return this.ownednotes.getMsectionid();
}
#OneToMany(mappedBy = "mnotedata",fetch = FetchType.LAZY,cascade = CascadeType.REMOVE)
#JsonIgnore
private Set<GroupAttachments> mattachments = new HashSet<>();
public Set<GroupAttachments> getMattachments() {
return this.mattachments;
}
public void setMattachments(Set<GroupAttachments> mattachments) {
this.mattachments = mattachments;
}
#OneToMany(mappedBy = "mhistory",fetch = FetchType.LAZY,cascade = CascadeType.REMOVE)
#JsonIgnore
private Set<GroupNoteHistory> groupNoteHistorySet = new HashSet<>();
public Set<GroupNoteHistory> getGroupNoteHistorySet(){
return this.groupNoteHistorySet;
}
public void setGroupNoteHistorySet(Set<GroupNoteHistory> groupNoteHistorySet){
this.groupNoteHistorySet = groupNoteHistorySet;
}
#OneToMany(mappedBy = "unreadNotes",fetch = FetchType.LAZY,cascade = CascadeType.REMOVE)
#JsonIgnore
private Set<UnreadNotes> unreadNotesSet = new HashSet<>();
public Set<UnreadNotes> getUnreadNotesSet(){
return this.unreadNotesSet;
}
public void setUnreadNotesSet(Set<UnreadNotes> unreadNotesSet){
this.unreadNotesSet = unreadNotesSet;
}
//getters and setters ignored
}
Error log :
org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: (was java.lang.NullPointerException) (through reference chain: java.util.ArrayList[0]->com.journaldev.spring.model.GroupNotes["ownedSectionId"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: java.util.ArrayList[0]->com.journaldev.spring.model.GroupNotes["ownedSectionId"])
Kindly let me know what to do, as I am stuck on that error since some time.
What I think that happens is Jackson tries to serialize all fields in the hierarchy based on getter methods. In some situation NullPointerException is thrown in the following method:
#JsonIgnore
public int getOwnedSectionId(){
return this.ownednotes.getMsectionid();
}
replace it with the following method:
#JsonIgnore
public int getOwnedSectionId(){
if(this.ownednotes != null)
return this.ownednotes.getMsectionid();
return 1;
}
I don't have an explanation why jackson tries to serialize it when is market with #JsonIgnore but you can give a try with my proposal
I would appreciate if anyone tells me either how to fix this error or convert the objects to a DTO objects so they can be transferred.
We use DozerMapper at work for this purpose.
Instead of doing that mapping manually you might want to take a look at Blaze-Persistence Entity Views which can be used to efficiently implement the DTO pattern with JPA. Here a quick code sample how your problem could be solved
First you define your DTO as entity view
#EntityView(GroupNotes.class)
public interface GroupNoteView {
#IdMapping("mnoticesid") int getId();
String getMnotecolor();
String getMnotetag();
String getMnotetext();
String getNoteDate();
Timestamp getNoteCreationTime();
int getAttachCount();
}
Next you adapt your DAO to make use of it
#Override
public List<GroupNoteView> searchNotesByDays(int days, int mcanvasid) {
EntityManager entityManager = // get the entity manager from somewhere
CriteriaBuilderFactory cbf = // factory for query building from Blaze-Persistence
EntityViewManager evm = // factory for applying entity views on query builders
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_YEAR, -days);
long daysAgo = cal.getTimeInMillis();
Timestamp nowMinusDaysAsTimestamp = new Timestamp(daysAgo);
CriteriaBuilder<GroupNotes> cb = cbf.create(entityManager, GroupNotes.class, "note")
.where("noteCreationTime").gt(nowMinusDaysAsTimestamp)
.where("ownednotes.ownedcanvas.mcanvasid").eq(mcanvasid);
return evm.applySetting(EntityViewSetting.create(GroupNoteView.class), cb)
.getResultList();
}
And finally the calling code
#RequestMapping(value = "/findgroupnotes/{days}/{canvasid}")
public #ResponseBody List<GroupNoteView> findGroupNotesByDays(#PathVariable("days")int days, #PathVariable("canvasid")int canvasid){
return this.groupNotesService.findGroupNotesByDays(days, canvasid);
}

Categories