spring boot mongodb #dbref cascade not working - json

I am struggling with Spring Boot MongoDB cascade operations on referenced objects. Below are MongoDB document schema classes.
== Post
#Getter
#Setter
#Document(collection="Post") // (1)
public class Post {
#Id
private String _id;
#Indexed(unique = true)
private Long id;
private String title;
private String body;
private Date createdDate;
#DBRef(db = "User", lazy = true)
private User user;
#DBRef(db = "Tag", lazy = true)
private Collection<Tag> tags;
== User
#Getter
#Setter
#Document(collection="User") // (1)
public class User {
#Id //(2)
private String _id;
#Indexed(unique = true)
private Long id;
#Indexed(unique=true) // (3)
private String username;
private String password;
private String email;
private String fullname;
private String role;
}
== Tag
#Getter
#Setter
#Document(collection="Tag")
public class Tag {
#Id
private String _id;
#Indexed(unique = true)
private Long mid;
private String body;
private Date createdDate;
#DBRef(db = "User", lazy = true)
private User user;
}
But #DBRef annotation does not work at all. It throws the following exception.
2019-03-01 14:54:10.411 ERROR 5756 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.data.mapping.MappingException: Cannot create a reference to an object with a NULL id.] with root cause
org.springframework.data.mapping.MappingException: Cannot create a reference to an object with a NULL id.
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.createDBRef(MappingMongoConverter.java:975) ~[spring-data-mongodb-2.1.4.RELEASE.jar:2.1.4.RELEASE]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writePropertyInternal(MappingMongoConverter.java:597) ~[spring-data-mongodb-2.1.4.RELEASE.jar:2.1.4.RELEASE]
When the json file is imported into MongoDB schema, the above error is shown. I found some reference site with googling which said to generate new event source using CascadingMongoEventListener class and user-defined #CascadeSave annotation. But I think there are another solutions with some cascade annotations. Any idea,please.

Mongo doesn't support the relationship between documents. Due to this cascade operation doesn't support in spring data mongo. you can do it in two manners.
1) Make your own cascade handler(best way to do is to use spring event publisher) But it can also be done using custom handler without spring event see here.
2) Make an explicit call to referenced DB for operation.

Take a look at RelMongo which is a framework built on top of Spring Data and which make possible cascading, fetching.. and even lookups which are not possible with DBRefs

Related

ObjectMapper.writeValueAsString JsonMappingException: failed to lazily initialize

There are two problems that I do not understand.
First, the error message on the console. It does not give me the whole error message. Therefore I do not understand the issue at all :S The IDE is STS.
Second, why do I get this error, "JsonMapping failed to lazily initialize a..."
#Test
public void testUpdateCar() throws Exception {
Car car = carRepository.findById(new Long(1)).get();
car.setBrand("Mazda");
car.setModel("326");
String putJSON = objectMapper.writeValueAsString(car);
mockMVC.perform(MockMvcRequestBuilders.put(String.format("/api/cars/%d", car.getId())).contentType(MediaType.APPLICATION_JSON_UTF8).content(putJSON))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.content().contentType("application/hal+json;charset=UTF-8"));
}
Car:
#Entity
public class Car {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToOne
private User owner;
private String brand;
private String model;
private String color;
private String plate;
private String additionals;
Update 1:
The error itself:
com.fasterxml.jackson.databind.JsonMappingException: failed to lazily
initialize a collection of role:
me.eraytuncer.carpool.entity.User.carList, could not initialize proxy
- no Session (through reference chain: me.eraytuncer.carpool.entity.Car["owner"]->me.eraytuncer.carpool.entity.User["carList"])
Update 2:
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String phone;
private String email;
#OneToMany(cascade = CascadeType.REMOVE, mappedBy = "owner")
private List<Car> carList;
Update 3:
#PutMapping("/cars/{id}")
ResponseEntity<?> replaceCar(#RequestBody Car newCar, #PathVariable Long id) {
if (repository.existsById(id)) {
newCar.setId(id);
Resource<Car> carResource = assembler.toResource(repository.save(newCar));
try {
return ResponseEntity
.created(new URI(carResource.getId().expand().getHref()))
.body(carResource);
} catch (URISyntaxException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
} else {
throw new CarNotFoundException(id);
}
}
Update 4:
Adding #JsonIgnore solved the issue somehow. Maybe it was a misleading issue caused by infinite recursion?
Looks like field
private List<Car> carList;
is resolved lazily (default fetch type in #OneToMany), which means it is populated from DB only when getter for this field is called. In your case it is called by Jackson serializer outside scope of the Hibernate session and property cannot be populated. Changing fetch type to EAGER in #OneToMany on carList property should help.
Also consider using DTO pattern, because returning entities from API is considered as bad practice.
You have mapped #OneToOne against #OneToMany. It should be #ManyToOne on the owning side:
#ManyToOne
#JoinColumn(name = "user_id") // if needed
private User owner;
I faced the same issue and it got resolved by using #Transactional on the method. #Transactional help to keep open the session so that lazy collection could be fetched.

Spring Boot Mapping a JSON to set a refereced object in the new that is being created

How to bind my new object user with an role object in my spring boot application when I receive an request post with a json/application that has all data for the new user?
What is the best approach (inform the role in the json)? If yes, how must be the json concerned the role information?
I will try to explain myself. First, I am sorry, I am not a native English speaker.
I want to create a new object user mapping the json received from a HTTP request post. The problem is that I have an internal object from my model named role. Roles are always either a common or an admin. Then I want to reference an already instantiated role object.
So, I want to know how to indicate the role from my user. You should consider that the model can not be modified because some internal team restriction. I don't know if the correct restful approach is send the role information in the json. How would you do this task?
My code
Class Controller
#RestController
public class UserController {
#RequestMapping(method=RequestMethod.POST, value="/users", produces = "application/json")
public #ResponseBody ResponseEntity<EntityUser> create(#Validated #RequestBody EntityUser user)
{
return new ResponseEntity<>(userService.add(user), HttpStatus.CREATED);
}
}
The Service class
#Service
public class UserService {
public EntityUser add(EntityUser user)
{
if (userRepository.findByName(user.getName()) == null)
return userRepository.save(user);
return null;
}
}
My plain object EntityUser (the json is mapped to it).
#Entity
public class EntityUser {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator="user_sequence")
private long id;
#Column(name = "name")
#NotNull
private String name;
#Column(name = "email", unique = true)
#NotNull
private String email;
#ManyToOne(fetch=FetchType.EAGER)
// #NotNull
private EntityRole role;
...
}
and finally my EntityRole class
#Entity
public class EntityRole {
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="role_sequence")
private long id;
#NotNull
private String label;
#NotNull
private String permission;
...
}

(Hibernate, mysql) Caused by: java.sql.SQLException: Field 'id' doesn't have a default value

I have an application which was running on H2 database and Hibernate is the ORM tool. Currently, I am changing this application to use mysql database instead of H2 database and while doing this I came to this issue when saving flagjp entity.
Here is the FlagJP entity that caused this issue.
#Entity
public class FlagJP extends BaseModelJP {
#Id
#GeneratedValue(generator = "IdOrGenerated")
#GenericGenerator(name = "IdOrGenerated", strategy = "com.jp.menu.api.model.JPSequenceGenerator")
private Long flagId;
private String flagKey;
#OneToMany(mappedBy="flag", cascade = CascadeType.ALL)
private List<FlagLangJP> flagLangs = new ArrayList<>();
#ManyToOne
private FlagCategoryJP flagCategory;
Here are the related entities for the FlagJP
Second entity
#Entity
public class FlagLangJP extends BaseModelJP {
#Id
#GeneratedValue(generator = "IdOrGenerated")
#GenericGenerator(name = "IdOrGenerated", strategy = "com.jp.menu.api.model.JPSequenceGenerator")
private Long id;
private String languageCode;
private String flagName;
private String flagDescription;
#ManyToOne
private FlagJP flag;
Third Entity
#Entity
public class FlagCategoryJP extends BaseModelJP {
#Id
#GeneratedValue(generator = "IdOrGenerated")
#GenericGenerator(name = "IdOrGenerated", strategy = "com.jp.menu.api.model.JPSequenceGenerator")
private Long flagCategoryId;
private String flagCategoryName;
#OneToMany(mappedBy = "flagCategory")
private List<FlagJP> flags;
While looking into this issue, I was able to figure out that this is cased by FlagJP table schema not having auto increment set in the database when hibernate generated the DDL.
here is the DDL of FlagJP
If I try to manually set the auto increment by executing a sql query, then mysql throw this error.
Operation failed: There was an error while applying the SQL script to the database.
ERROR 1833: Cannot change column 'flagId': used in a foreign key constraint 'FK_sk95esyf1n0gt1qqmlmdmq0uw' of table 'butterfly_emenu.flaglangbpa'
SQL Statement:
my question is , this problem does not happen when using H2 database. how to solve this issue using hibernate when the database is mysql.
Thanks in advance for any advice
Update:
Here is the code for sequence generator I am using
public class JPSequenceGenerator extends IdentityGenerator {
private static final Logger logger = LoggerFactory.getLogger(JPSequenceGenerator.class);
#Override
public Serializable generate(SessionImplementor session, Object object) throws HibernateException {
Serializable id = session.getEntityPersister(null, object).getClassMetadata().getIdentifier(object, session);
if (id == null) {
id = super.generate(session, object);
}
return id;
}
}
Try below code with auto_increment field ID in mysql
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long flagId;
If you are not able to add auto_increment for flagId then remove the foreignKey FK_sk95esyf1n0gt1qqmlmdmq0uw then add auto_increment and add foreign key again

Could not write content: failed to lazily initialize a collection of role

I have One-To-Many relationship, here is my code
#Entity
#Table(name = "catalog")
public class Catalog {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "catalog_id")
private int catalog_id;
#NotEmpty
#Size(min = 3, max = 255)
#Column(name = "name", nullable = false)
private String name;
#OneToMany(mappedBy="mycatalogorder")
private List<Order> orders;
#OneToMany(mappedBy="mycatalog")
private List<CatalogItem> items;
// setters and getters
}
#Entity
#Table(name = "catalogitem")
public class CatalogItem {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "catalogitem_id")
private int catalogitem_id;
#NotEmpty
#Size(min = 3, max = 255)
#Column(name = "name", nullable = false)
private String name;
#NotEmpty
#Column(name = "price", nullable = false)
private Double price;
#OneToOne(mappedBy="ordercatalogitem", cascade=CascadeType.ALL)
private OrderItem morderitem;
#ManyToOne
#JoinColumn(name="catalog_id", nullable=false)
private Catalog mycatalog;
// setters and getters
}
#Entity
#Table(name = "orders")
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "order_id")
private int order_id;
#NotEmpty
#Size(min = 3, max = 255)
#Column(name = "name", nullable = false)
private String name;
#NotEmpty
#Size(min = 3, max = 1024)
#Column(name = "note", nullable = false)
private String note;
#Temporal(TemporalType.TIMESTAMP)
#DateTimeFormat(pattern = "ddmmYYYY HH:mm:ss")
#Column(name = "created", nullable = false)
private Date created;
#OneToMany(mappedBy="myorder")
private Set<OrderItem> orderItems;
#ManyToOne
#JoinColumn(name="catalog_id", nullable=false)
private Catalog mycatalogorder;
#PrePersist
protected void onCreate() {
created = new Date();
}
// setters and getters
}
#Entity
#Table(name = "orderitem")
public class OrderItem {
#Id
#Column(name="catalogitem_id", unique=true, nullable=false)
#GeneratedValue(generator="gen")
#GenericGenerator(name="gen", strategy="foreign", parameters=#Parameter(name="property", value="catalogitem"))
private int catalogitem_id;
#Column(name = "quantity")
private int quantity;
#OneToOne
#PrimaryKeyJoinColumn
private CatalogItem ordercatalogitem;
#ManyToOne
#JoinColumn(name="order_id", nullable=false)
private Order myorder;
// setters and getters
}
And I am getting the exception:
org.springframework.http.converter.HttpMessageNotWritableException:
Could not write content: failed to lazily initialize a collection of
role: com.example.helios.model.Catalog.items, could not initialize
proxy - no Session; nested exception is
com.fasterxml.jackson.databind.JsonMappingException: failed to lazily
initialize a collection of role:
com.example.helios.model.Catalog.items, could not initialize proxy -
no Session
org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:271)
org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:100)
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:222)
org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:183)
org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:80)
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:126)
My versions is:
SpringFramework 4.2.4.RELEASE
Hibernate 4.3.11.Final
Jackson 2.7.4
Jacksontype 2.7.1
This is the normal Hibernate behaviour
In one to many relations, hibernate loads the father entity (Catalog in your case) but it will load the children entities List (List items and List orders in your case) in a LAZY mode
This means you can't access to these objects because they are just proxies and not real objects
This is usefull in order to avoid to load the full DB when you execute a query
You have 2 solution:
Load children entities in EAGER mode (I strongly suggest to you to not do it because you can load the full DB.... but it is something related to your scenario
You don't serialize in your JSON the children entities by using the com.fasterxml.jackson.annotation.JsonIgnore property
Angelo
A third option which can be useful if you don't want to use EAGER mode and load up everything is to use Hibernate::initialize and only load what you need.
Session session = sessionFactory.openSession();
Catalog catalog = (Catalog) session.load(Catalog.class, catalogId);
Hibernate.initialize(shelf);
More information
I had the same problem but a fixed by:
#OneToMany
#JoinColumn(name = "assigned_ingredient", referencedColumnName = "ingredient_id")
#Fetch(FetchMode.JOIN) // Changing the fetch profile you can solve the problem
#Where(clause = "active_ind = 'Y'")
#OrderBy(clause = "meal_id ASC")
private List<Well> ingredients;
you can have more information here: https://vladmihalcea.com/the-best-way-to-handle-the-lazyinitializationexception/
It's caused by an infinite loop when parsing datas to JSON.
You can solve this by using #JsonManagedReference and #JsonBackReference annotations.
Definitions from API :
JsonManagedReference (https://fasterxml.github.io/jackson-annotations/javadoc/2.5/com/fasterxml/jackson/annotation/JsonManagedReference.html) :
Annotation used to indicate that annotated property is part of two-way
linkage between fields; and that its role is "parent" (or "forward")
link. Value type (class) of property must have a single compatible
property annotated with JsonBackReference. Linkage is handled such
that the property annotated with this annotation is handled normally
(serialized normally, no special handling for deserialization); it is
the matching back reference that requires special handling
JsonBackReference: (https://fasterxml.github.io/jackson-annotations/javadoc/2.5/com/fasterxml/jackson/annotation/JsonBackReference.html):
Annotation used to indicate that associated property is part of
two-way linkage between fields; and that its role is "child" (or
"back") link. Value type of the property must be a bean: it can not be
a Collection, Map, Array or enumeration. Linkage is handled such that
the property annotated with this annotation is not serialized; and
during deserialization, its value is set to instance that has the
"managed" (forward) link.
Example:
Owner.java:
#JsonManagedReference
#OneToMany(mappedBy = "owner", fetch = FetchType.EAGER)
Set<Car> cars;
Car.java:
#JsonBackReference
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "owner_id")
private Owner owner;
Another solution is to use #JsonIgnore which will just set null to the field.
Here is my solution for this task with Hibernate. I marked hibernate releation with #JsonIgnore and use custom field for jackson, in which I check if the field is loaded. If you need serialize collection to json then you should manualy call collection getter during hibernate transaciton.
#JsonIgnore
#OneToMany(mappedBy = "myorder")
private List<OrderItem> orderItems = new ArrayList<>();
#JsonProperty(value = "order_items", access = JsonProperty.Access.READ_ONLY)
private List<OrderItem> getOrderItemsList() {
if(Hibernate.isInitialized(this.relatedDictionary)){
return this.relatedDictionary;
} else{
return new ArrayList<>();
}
}
#JsonProperty(value = "order_items", access = JsonProperty.Access.WRITE_ONLY)
private void setOrderItemsList(List<OrderItem> orderItems) {
this.orderItems = orderItems;
}
I know this is an old post but this might still help someone facing a similar issue. To solve the problem, iterate through the list of items and set the lazy-loadable collection to null. Then set your mapper to include NON-NULL
for (Catalog c : allCatalogs) {
c.setItems(null);
}
objectMapper.setSerializationInclusion(Include.NON_NULL)
Using FetchType.LAZY , if still getting the error "Could not write content: failed to lazily initialize a collection of role" , that may be probably caused by somewhere in the logic (perhaps in a controller) , Catalog is being tried to be deserialized that contains list of catalog items which is a proxy but the transaction has already ended to get that.
So create a new model ('CatalogResource' similar to catalog but without the list of items).
Then create a catalogResource object out of the Catalog (which is returned from the query)
public class CatalogResource {
private int catalog_id;
private String name;
private List<Order> orders;
}
I think the best solution to your problem (which also is the simplest) is to set your FetchType to LAZY and simply annotate the oneToMany collection fields using #transient.
Setting FetchType to EAGER isn't a good idea most times.
Best of luck.
"You don't serialize in your JSON the children entities by using the com.fasterxml.jackson.annotation.JsonIgnore property"
Add #JsonIgnore for hibernate lazy loading properties eg. #ManyToOne. That should work

Spring DATA REST JSON mapper to Domain Model

I have the problem, I am using Spring Data Rest. So i have some Domain Model like this:
#Entity
public class Sample implements Serializable {
#Id
#GeneratedValue
private Long id;
#Column(name = "name")
private String name;
#Column(name = "is_main")
private Boolean isMain;
#LastModifiedDate
#Column(name = "last_modified")
private Date lastModified;
#ManyToOne
#JoinColumn(nullable = false,name = "user_id")
private User user;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(nullable = false,name="state_id")
private SampleState state;
}
So from client I am sending POST request to http://{server.host}:8080/samples
to create Sample with JSON:
{"name":"sample","user":{"id":1},"state":{"id":1}}
Hoping that it will automatically map "user" of json to Model param "user", but HttpMessageConverter just ignore the "state" JSON Object and "user" Json Object. Can you help how can I manage to Customize converter or any other ways to create sample?
Thanks in advance!
What you mean by ignore?
BTW, if you have 'user' and 'state' objects with the 'id' value '1'; then the association should persisted with new sample object successfully in the database.
But if you want to create the whole object graph into db, then the id value should be null and perhaps you might need to setup the proper cascade options.