I am pretty new to mongo so this is probably just me not understanding how to setup collections. I have 2 domain objects and I have stripped them down to the basics for simplicity.
Post.java
#Document
public class Post {
#Id
private BigInteger id;
private String title;
private String body;
private String teaser;
private String slug;
private Date postedOn;
private Author author;
public Post(){
}
// getters & setters
}
Author.java
#Document
public class Author {
private BigInteger id;
private String firstName;
private String lastName;
private String email;
#DBRef
private List<Post> posts;
public Author(){
}
// getters and setters
}
I have a method that gets called to load some data into the database.
#PostConstruct
private void initDatabase(){
authorRepository.deleteAll();
Author dv = new Author();
dv.setFirstName("Dan");
dv.setLastName("Vega");
dv.setEmail("danvega#gmail.com");
authorRepository.save( dv );
postRepository.deleteAll();
Post post = new Post();
post.setTitle("Spring Data Rocks!");
post.setSlug("spring-data-rocks");
post.setTeaser("Post Teaser");
post.setBody("Post Body");
post.setPostedOn(new Date());
post.setAuthor(dv);
postRepository.save(post);
}
When I run mongo from the console and show collections I see both the post and author collections.
When I run db.post.find() the post contains the author object
{ "_id" : ObjectId("5666201fd4c6bcfd2f4caa90"), "_class" : "com.therealdanvega.domain.Post", "title" : "Spring Data Rocks!", "body" : "Post Body", "teaser" : "Post Teaser", "slug" : "spring-data-rocks", "postedOn" : ISODate("2015-12-08T00:11:11.090Z"), "author" : { "_id" : ObjectId("5666201fd4c6bcfd2f4caa8f"), "firstName" : "Dan", "lastName" : "Vega", "email" : "danvega#gmail.com" } }
But when I run db.author.find() I don't see the post collection in there.
{ "_id" : ObjectId("5666201fd4c6bcfd2f4caa8f"), "_class" : "com.therealdanvega.domain.Author", "firstName" : "Dan", "lastName" : "Vega", "email" : "danvega#gmail.com" }
Does anyone know what I am missing?
When you save you author object there is no post inside.
Create your Author, and create you post as you do.
Then set the post just created into the author's post list and save it.
This should do the trick
#PostConstruct
private void initDatabase(){
authorRepository.deleteAll();
Author dv = new Author();
dv.setFirstName("Dan");
dv.setLastName("Vega");
dv.setEmail("danvega#gmail.com");
authorRepository.save( dv );
postRepository.deleteAll();
Post post = new Post();
post.setTitle("Spring Data Rocks!");
post.setSlug("spring-data-rocks");
post.setTeaser("Post Teaser");
post.setBody("Post Body");
post.setPostedOn(new Date());
post.setAuthor(dv);
postRepository.save(post);
// add the code below
dv.posts = new ArrayList<Post>();
dv.posts.add(post);
authorRepository.save( dv );
}
Related
we are using spring-boot 2.2.1 and query-dsl-mongoDB 4.2.1.
we are using spring-data.mongodb findAll method tp find books from book collection using
various predicates like bookUid, authorId, customerId, status, isbn and provisioningId.
I can able to construct for all the attributes except bookInfo.
Please find the sample collection for reference.
{
"_id" : ObjectId("6036323daa819c04005cff68"),
"bookUid" : "spring_boot",
"authorId" : "602bc44827e37ca2ba281f54",
"customerId" : "75e1c48e",
"status" : "ACTIVE",
"name" : "Spring boot",
"statusTimestamp" : ISODate("2021-02-24T11:07:28.000Z"),
"deleted" : false,
"bookInfo" : {
"isbn" : "240220211202",
"provisioningId" : "240220211202"
},
"customInfo" : {},
"version" : 1,
"countryCode" : "CZ",
"_class" : "book-collection"
}
And this is the Java class,
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class Book {
#Id
private String id;
#Indexed
#WhiteSpaceTrim
private String bookUid;
private String authorId;
private String customerId;
private String status;
private String name;
private Date statusTimestamp;
private boolean deleted;
#WhiteSpaceTrim
private Map<String, String> provisionInfo;
private SmartObject customInfo;
private int version;
private String countryCode;
}
While looking into query DSL autogenerated class I see the types for BookInfo is QMap
public final ext.java.util.QMap provisionInfo = new
ext.java.util.QMap(forProperty("bookInfo"));
I tried to construct below predicate whether bookInfo matches the bey key and value
Map<String, String> expre = new HashMap<>();
expre.put(key, value);
predicates.add(QBook.book.provisionInfo.in(expre).isTrue());
But no luck it was not working and thrown exception, Then tried following expression
PathBuilder entityPath = new PathBuilder<>(Book.class,
"bookInfo");
predicates.add(entityPath.getMap("map", String.class, String.class).get(key).eq(value));
But it returns an empty collection always even though we have matching isbn or provisioningId.
I was looking into the documentation but I couldn't find any help either.
Any help would be really appreciable.
I have read several tutorial about one-to-one relation mapping forexample:
https://docs.oracle.com/javaee/6/api/javax/persistence/OneToOne.html,
http://websystique.com/hibernate/hibernate-one-to-one-unidirectional-with-foreign-key-associations-annotation-example/
https://en.wikibooks.org/wiki/Java_Persistence/OneToOne
I beleive I follow these tutorials, however my relational mapping still not works as expected. I have the following classes:
#Entity(name = "lesson")
public class Lesson {
#Id
#Type(type = "pg-uuid")
private UUID uid;
private String start_date_time;
private String end_date_time;
private String location;
#OneToOne
#JoinColumn(name="uid") //uid is the name of the Id i want to reference to in the subject class
private Subject subject_uid; // subject_uid is the name of the column in my subject table
public Lesson(UUID uid, String start_date_time, String end_date_time, String location, Subject subject_uid) {
this.uid = uid;
this.start_date_time = start_date_time;
this.end_date_time = end_date_time;
this.location = location;
this.subject_uid = subject_uid;
}
//getters setters
#Entity(name = "subject")
public class Subject {
#Id
#Type(type = "pg-uuid")
private UUID uid;
private String name;
private String start_date;
private String end_date;
private boolean is_lesson_created;
public Subject(UUID uid, String name, String start_date, String end_date, boolean is_lesson_created) {
this.uid = uid;
this.name = name;
this.start_date = start_date;
this.end_date = end_date;
this.is_lesson_created = is_lesson_created;
}
The response what the Spring Data Rest creates on /lessons endpoint looks the following:
{
"_embedded" : {
"lessons" : [ {
"start_date_time" : "2017-01-08 08:30:00",
"end_date_time" : "2017-01-08 10:15:00",
"location" : "A101 ",
"_links" : {
"self" : {
"href" : "http://localhost:3400/lessons/78038aeb-cdc9-4673-990e-36b8c1105500"
},
"lesson" : {
"href" : "http://localhost:3400/lessons/78038aeb-cdc9-4673-990e-36b8c1105500"
},
"subject_uid" : {
"href" : "http://localhost:3400/lessons/78038aeb-cdc9-4673-990e-36b8c1105500/subject_uid"
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://localhost:3400/lessons{?page,size,sort}",
"templated" : true
},
"profile" : {
"href" : "http://localhost:3400/profile/lessons"
}
},
"page" : {
"size" : 20,
"totalElements" : 1,
"totalPages" : 1,
"number" : 0
}
}
When I want access the http://localhost:3400/lessons/78038aeb-cdc9-4673-990e-36b8c1105500/subject_uidlink I get a 404.
Is the UUID type effects my mapping? What should I change to be able to access my student_uid?
Finally I found out the problem, which is something I haven't read anywhere. When a one-to-one join has to be done, JPA provides the default name of the join as the table name underscore id(subject_id). In my case, I have a tablename called "subject" in the database and the PK called simply "uid". So what you have to do is append the table name with the name of the attribute, which to join to:
#OneToOne
#JoinColumn(name="subject_uid")//the pattern is: "tablename_joined attribute"
private Subject subject_uid;
Is it possible to create a global DTO for an xml webservice, but having conditional fields inside?
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
#JsonInclude(JsonInclude.Include.NON_EMPTY)
public class MyDTO {
...
#XmlElementWrapper
#XmlElement(name = "somename")
private List<String> list;
}
Now, what if I want to release another version of the webservice, and rename the #XmlElement field thereby (or introduce additional fields, remove some, etc).
So that backwards compatibility is retained, but the same objects are used for the "new" version.
I could maybe do this my adding request path methods with /v1, /v2 etc.
But how could I then maintain a single DTO class, but with fields conditional on the version path?
Or would I always have to duplicate those DTO classes and modify exactly to me needs of the version?
#RestController
public void MyServlet {
#RequestMapping("/v1")
public MyDTO1 request1() {
}
#RequestMapping("/v2")
public MyDTO2 request2() {
}
}
I would prefer using tailored DTOs for each version of the API. To avoid boilerplate code when mapping your entities to DTOs, you could consider using mapping frameworks such as MapStruct.
If you are using Jackson, you could consider using JSON Views (they will work with XML too). Quoting from the Latest Jackson integration improvements in Spring article:
JSON Views
It can sometimes be useful to filter contextually objects serialized to the HTTP response body. In order to provide such capabilities, Spring MVC now has builtin support for Jackson’s Serialization Views (as of Spring Framework 4.2, JSON Views are supported on #MessageMapping handler methods as well).
The following example illustrates how to use #JsonView to filter fields depending on the context of serialization - e.g. getting a “summary” view when dealing with collections, and getting a full representation when dealing with a single resource:
public class View {
interface Summary {}
}
public class User {
#JsonView(View.Summary.class)
private Long id;
#JsonView(View.Summary.class)
private String firstname;
#JsonView(View.Summary.class)
private String lastname;
private String email;
private String address;
private String postalCode;
private String city;
private String country;
}
public class Message {
#JsonView(View.Summary.class)
private Long id;
#JsonView(View.Summary.class)
private LocalDate created;
#JsonView(View.Summary.class)
private String title;
#JsonView(View.Summary.class)
private User author;
private List<User> recipients;
private String body;
}
Thanks to Spring MVC #JsonView support, it is possible to choose, on a per handler method basis, which field should be serialized:
#RestController
public class MessageController {
#Autowired
private MessageService messageService;
#JsonView(View.Summary.class)
#RequestMapping("/")
public List<Message> getAllMessages() {
return messageService.getAll();
}
#RequestMapping("/{id}")
public Message getMessage(#PathVariable Long id) {
return messageService.get(id);
}
}
In this example, if all messages are retrieved, only the most important fields are serialized thanks to the getAllMessages() method annotated with #JsonView(View.Summary.class):
[ {
"id" : 1,
"created" : "2014-11-14",
"title" : "Info",
"author" : {
"id" : 1,
"firstname" : "Brian",
"lastname" : "Clozel"
}
}, {
"id" : 2,
"created" : "2014-11-14",
"title" : "Warning",
"author" : {
"id" : 2,
"firstname" : "Stéphane",
"lastname" : "Nicoll"
}
}, {
"id" : 3,
"created" : "2014-11-14",
"title" : "Alert",
"author" : {
"id" : 3,
"firstname" : "Rossen",
"lastname" : "Stoyanchev"
}
} ]
In Spring MVC default configuration, MapperFeature.DEFAULT_VIEW_INCLUSION is set to false. That means that when enabling a JSON View, non annotated fields or properties like body or recipients are not serialized.
When a specific Message is retrieved using the getMessage() handler method (no JSON View specified), all fields are serialized as expected:
{
"id" : 1,
"created" : "2014-11-14",
"title" : "Info",
"body" : "This is an information message",
"author" : {
"id" : 1,
"firstname" : "Brian",
"lastname" : "Clozel",
"email" : "bclozel#pivotal.io",
"address" : "1 Jaures street",
"postalCode" : "69003",
"city" : "Lyon",
"country" : "France"
},
"recipients" : [ {
"id" : 2,
"firstname" : "Stéphane",
"lastname" : "Nicoll",
"email" : "snicoll#pivotal.io",
"address" : "42 Obama street",
"postalCode" : "1000",
"city" : "Brussel",
"country" : "Belgium"
}, {
"id" : 3,
"firstname" : "Rossen",
"lastname" : "Stoyanchev",
"email" : "rstoyanchev#pivotal.io",
"address" : "3 Warren street",
"postalCode" : "10011",
"city" : "New York",
"country" : "USA"
} ]
}
Only one class or interface can be specified with the #JsonView annotation, but you can use inheritance to represent JSON View hierarchies (if a field is part of a JSON View, it will be also part of parent view). For example, this handler method will serialize fields annotated with #JsonView(View.Summary.class) and #JsonView(View.SummaryWithRecipients.class):
public class View {
interface Summary {}
interface SummaryWithRecipients extends Summary {}
}
public class Message {
#JsonView(View.Summary.class)
private Long id;
#JsonView(View.Summary.class)
private LocalDate created;
#JsonView(View.Summary.class)
private String title;
#JsonView(View.Summary.class)
private User author;
#JsonView(View.SummaryWithRecipients.class)
private List<User> recipients;
private String body;
}
#RestController
public class MessageController {
#Autowired
private MessageService messageService;
#JsonView(View.SummaryWithRecipients.class)
#RequestMapping("/with-recipients")
public List<Message> getAllMessagesWithRecipients() {
return messageService.getAll();
}
}
I am stuck on something and after a day of searching and trying different things I am throwing in the towel. I have 2 basic domains, a blog post and an author. I have left a little code out to keep this post short.
#Entity
public class Post {
#Id #GeneratedValue
private Long id;
private String title;
#Column(columnDefinition = "TEXT")
private String body;
#Column(columnDefinition = "TEXT")
private String teaser;
private String slug;
#CreatedDate
#Temporal(TemporalType.TIMESTAMP)
private Date postedOn;
#ManyToOne
private Author author;
// getters & setters
}
#Entity
public class Author {
#Id
#GeneratedValue
private Long id;
private String firstName;
private String lastName;
private String email;
// getters & setters
}
The controller looks like this
#RestController
#RequestMapping("/posts")
public class PostController {
private PostService postService;
#Autowired
public PostController(PostServiceImpl postService){
this.postService = postService;
}
#RequestMapping( value = "/", method = RequestMethod.GET )
public Iterable<Post> list(){
return postService.list();
}
#RequestMapping( value = "/", method = RequestMethod.POST )
public Post create(#RequestBody Post post){
return postService.save(post);
}
#RequestMapping( value = "/{id}", method = RequestMethod.GET )
public Post read(#PathVariable(value="id") long id){
return postService.getPost(id);
}
#RequestMapping( value = "/{id}", method = RequestMethod.PUT )
public String update(#PathVariable(value="id") int id){
return "post.update()";
}
#RequestMapping( value = "/{id}", method = RequestMethod.DELETE )
public String delete(#PathVariable(value="id") int id){
return "post.delete()";
}
}
And all the service method does is take the Post POJO and call the save method on the repository. This is my question and I feel dumb for even asking it. When I post JSON from Postman with no author (null) everything works fine. I am just not sure how the heck to post a json object with a new author or an existing one.
This works
{
"title" : "A new post created from JSON",
"slug" : "a-new-post",
"teaser" : "post teaser",
"body" : "post body",
"postedOn" : "2015-11-07"
}
When I try and post this JSON
{
"title" : "A new post created from JSON",
"slug" : "a-new-post",
"teaser" : "post teaser",
"body" : "post body",
"postedOn" : "2015-11-07",
"author" : {
"firstName": "Joe",
"lastName": "Smith",
"email": "jsmith#gmail.com"
}
}
I get the following error
{
"timestamp": 1447018768572,
"status": 500,
"error": "Internal Server Error",
"exception": "org.springframework.dao.InvalidDataAccessApiUsageException",
"message": "org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.therealdanvega.domain.Post.author -> com.therealdanvega.domain.Author; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.therealdanvega.domain.Post.author -> com.therealdanvega.domain.Author",
"path": "/posts/"
}
You first need to persist the Author. From the Post point of view, the Author you are presenting to it is Transient as it has no id, thus mapping cannot be done between Post and Author.
So create a persistent object of the Author and then insert it into the Post entity.
Given Person.java:
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String lastName;
private String firstName;
private String middleName;
// ...
public String getLastName() {
return lastName;
}
public String getFirstName() {
return firstName;
}
public String getMiddleName() {
return middleName;
}
}
and Persons.java:
public class Persons implements Serializable {
private static final long serialVersionUID = 1L;
private final List<Person> persons;
public Persons(List<Person> persons) {
this.persons = persons;
}
public List<Person> getPersons() {
return persons;
}
}
When returning a JSON response for Persons, the "persons" element is repeated:
{
"persons" : {
"persons" : [ {
"lastName" : "McCartney",
"firstName" : "James",
"middleName" : "Paul"
}, {
"lastName" : "Lennon",
"firstName" : "John",
"middleName" : "Winston"
}, {
"lastName" : "Starkey",
"firstName" : "Richard",
"middleName" : null
}, {
"lastName" : "Harrison",
"firstName" : "George",
"middleName" : null
} ]
}
}
How do I remove the extra element?
The reason is probably because you have an model attribute named persons which you are returning for MappingJackson2JsonView to convert to json:
model.addAttribute("persons", personsType);
There are two good fixes that I can think of:
To use #ResponseBody annotated controller methods instead, this way you can return persons and MappingJackson2HttpMessageConverter would convert your type cleanly to json
If you want to continue with your approach, you can customize MappingJackson2JsonViewwith an additional flag to indicate that it has to extract value from the model before serializing to json - see here