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.
Related
I have 2 entities, with 1-to-1 association (ProfileEntity and VCardEntity)
Entity vcard:
#Entity
#Table(name = "vcard")
#AllArgsConstructor
#NoArgsConstructor
#Data
#SequenceGenerator(name="vcard_id_seq_generator", sequenceName="vcard_id_seq", allocationSize = 1)
public class VCardEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "vcard_id_seq_generator")
#Column(name="vcard_id")
Long id;
String account;
#Column(name = "first_name")
String firstName;
#Column(name = "last_name")
String lastName;
#Column(name = "pbxinfo_json")
String pbxInfoJson;
#Column(name = "avatar_id")
String avatarId;
#OneToOne(mappedBy = "vcard")
ProfileEntity profile;
}
entity Profile:
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Data
#Table(name = "profile")
public class ProfileEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "profile_id")
private Long profileId;
private String account;
#Column(name = "product_id")
private String productId;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "vcard_id", referencedColumnName = "vcard_id")
private VCardEntity vcard;
}
I use map struct as follow:
public class CycleAvoidingMappingContext {
private Map<Object, Object> knownInstances = new IdentityHashMap<Object, Object>();
#BeforeMapping
public <T> T getMappedInstance(Object source, #TargetType Class<T> targetType) {
return targetType.cast(knownInstances.get(source));
}
#BeforeMapping
public void storeMappedInstance(Object source, #MappingTarget Object target) {
knownInstances.put( source, target );
}
}
#Mapper(componentModel = "spring")
public interface EntityToProfile {
ProfileEntity profileToEntity(Profile profile, #Context CycleAvoidingMappingContext context);
Profile entityToProfile(ProfileEntity entity, #Context CycleAvoidingMappingContext context);
}
#Mapper(componentModel = "spring")
public interface EntityToVCard {
VCard entityToVcard(VCardEntity entity, #Context CycleAvoidingMappingContext context);
VCardEntity vcardToEntity(VCard vcard, #Context CycleAvoidingMappingContext context);
}
Finally i call mapping in my service:
#Service
#RequiredArgsConstructor
#Slf4j
public class DefaultChatService implements ChatService {
private final ProfileRepository profileRepository;
private final EntityToProfile entityToProfileMapper;
private final EntityToVCard entityToVCardMapper;
#Override
public List<Profile> findAllProfile(Optional<Long> id) {
if (id.isPresent()) {
Optional<ProfileEntity> result = profileRepository.findById(id.get());
if (result.isPresent()) {
Profile profile = entityToProfileMapper.entityToProfile(result.get(), new CycleAvoidingMappingContext());
return Stream.of(profile).collect(Collectors.toList());
}
}
return new ArrayList<Profile>();
}
}
and i got the error
ERROR 15976 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.StackOverflowError] with root cause
java.lang.StackOverflowError: null
Any thoughts how can i fix it?
In my view i did everything as it's written here Prevent Cyclic references when converting with MapStruct but it doesn't work for me
Found a solution, all i had to do was to change from #Value to #Data for my models
example given:
#Data
public class Profile {
Long profileId;
String account;
String productId;
VCard vcard;
}
and
#Data
public class VCard {
Long id;
String account;
String firstName;
String lastName;
String pbxInfoJson;
String avatarId;
Profile profile;
}
Otherwise mapstruct could not generate proper mapping code. It was trying to store an instance in knownInstances after creating an object, for example Profile. But because #value doesn't provide a way to set properties after creating the object (immutable object) it had to create all settings first and then use all args constructor which led to a mapping profile first which in turn was trying to do the same and map vcard first before storing the VCard object in knownInstances.
This is why the cyclic reference problem could not be solved
Properly generated code:
public Profile entityToProfile(ProfileEntity entity, CycleAvoidingMappingContext context) {
Profile target = context.getMappedInstance( entity, Profile.class );
if ( target != null ) {
return target;
}
if ( entity == null ) {
return null;
}
Profile profile = new Profile();
context.storeMappedInstance( entity, profile );
profile.setAccount( entity.getAccount() );
profile.setProductId( entity.getProductId() );
profile.setDeviceListJson( entity.getDeviceListJson() );
profile.setLastSid( entity.getLastSid() );
profile.setBalanceValue( entity.getBalanceValue() );
profile.setBalanceCurrency( entity.getBalanceCurrency() );
profile.setStatusJson( entity.getStatusJson() );
profile.setData( entity.getData() );
profile.setMissedCallsCount( entity.getMissedCallsCount() );
profile.setFirstCallSid( entity.getFirstCallSid() );
profile.setLastMissedCallSid( entity.getLastMissedCallSid() );
profile.setRemoveToCallSid( entity.getRemoveToCallSid() );
profile.setOutgoingLines( entity.getOutgoingLines() );
profile.setFeatures( entity.getFeatures() );
profile.setPermissions( entity.getPermissions() );
profile.setVcard( vCardEntityToVCard( entity.getVcard(), context ) );
return profile;
}
}
As you can see, firstly, it saves the object in context.storeMappedInstance( entity, profile ); and then fills the properties.
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);
}
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/
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);
}
I want to convert the following json into a java object, using as much as possible annotations.
{"user":{
"id":1,
"diets":[
{"diet":{
"name":"...",
"meals":[]
}
}
]
}
}
I'm getting trouble with the collection diets. I tried to use #JsonProperty but it doesn't work properly. Is there a special annotation for map inner aggregates?
Diet.java
#JsonRootName(value = "diet")
public class Diet {
#JsonProperty(value="name")
private String name;
#JsonProperty(value="meals")
private List<Meal> meals;
private User user;
// Rest of the class omitted.
}
User.java
#JsonRootName(value = "user")
public class User {
#JsonProperty("id")
private long id;
#JsonProperty("diets")
private List<Diet> diets = new ArrayList<Diet>();
// Rest of the class omitted.
}
Thanks!
The diets object in your json is not a List. Its a List of key-value pair with key "diet" and value a diet object. So you have three options here.
One is to create a wrapper object say DietWrapper and use List of diet wrapper in User like
#JsonRootName(value = "user")
class User {
#JsonProperty(value = "id")
private long id;
#JsonProperty(value = "diets")
private List<DietWrapper> diets;
//Getter & Setters
}
class DietWrapper {
#JsonProperty(value = "diet")
Diet diet;
}
Second option is to keep diest as simple list of maps like List>
#JsonRootName(value = "user")
class User {
#JsonProperty(value = "id")
private long id;
#JsonProperty(value = "diets")
private List<Map<String, Diet>> diets;
//Getter & Setters
}
Third option is to use a custom deserializer which would ignore your diet class.
#JsonRootName(value = "user")
class User {
#JsonProperty(value = "id")
private long id;
#JsonProperty(value = "diets")
#JsonDeserialize(using = DietDeserializer.class)
private List<Diet> diets;
//Getter & Setters
}
class DietDeserializer extends JsonDeserializer<List<Diet>> {
#Override
public List<Diet> deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(jsonParser);
List<Diet> diets = mapper.convertValue(node.findValues("diet"), new TypeReference<List<Diet>>() {});
return diets;
}
}