I am trying to create a springboot application using MongoDB and a Rest controller and connect objects together using DBRef instead of classic Jpa annotations like OneToMany etc. The purpose is to print all the bookmarks for a specific account. The list of bookmarks is found by the username but it seems that it doesn't work.
These are my classes:
#Document
public class Account {
#DBRef
private Set<Bookmark> bookmarkSet = new HashSet<>();
#Id
private String id;
#JsonIgnore
private String username;
private String password;
public Account(String username, String password) {
this.username = username;
this.password = password;
}
public void setBookmarkSet(Set<Bookmark> bookmarkSet) {
this.bookmarkSet = bookmarkSet;
}
public String getId() {
return id;
}
}
#Document
public class Bookmark {
#DBRef
#JsonIgnore
private Account account;
#Id
private String id;
private String uri;
private String description;
public Bookmark(Account account, String uri, String description) {
this.account = account;
this.uri = uri;
this.description = description;
}
public Account getAccount() {
return account;
}
public String getId() {
return id;
}
public String getUri() {
return uri;
}
public String getDescription() {
return description;
}
}
repositories:
public interface AccountRepository extends MongoRepository<Account, Long> {
Optional<Account> findOneByUsername(String username);
}
public interface BookmarkRepository extends MongoRepository<Bookmark, Long> {
Collection<Bookmark> findByAccountUsername(String username);
}
And RestController:
#RestController
#RequestMapping("/{userId}/bookmarks")
public class BookmarkRestController {
private final AccountRepository accountRepository;
private final BookmarkRepository bookmarkRepository;
#Autowired
public BookmarkRestController(AccountRepository accountRepository, BookmarkRepository bookmarkRepository) {
this.accountRepository = accountRepository;
this.bookmarkRepository = bookmarkRepository;
}
#RequestMapping(value = "/{bookmarkId}", method = RequestMethod.GET)
Bookmark readBookmark(#PathVariable String userId, #PathVariable Long bookmarkId) {
this.validateUser(userId);
return bookmarkRepository.findOne(bookmarkId);
}
#RequestMapping(method = RequestMethod.GET)
Collection<Bookmark> readBookmarks(#PathVariable String userId) {
this.validateUser(userId);
return this.bookmarkRepository.findByAccountUsername(userId);
}
private void validateUser(String userId) {
this.accountRepository.findOneByUsername(userId).orElseThrow(() -> new UserNotFoundException(userId));
}
}
After I run the application I get this error:
Invalid path reference account.username! Associations can only be pointed to directly or via their id property!
I'm not sure you have the right schema design. I assume you've modeled you objects based on a relational database type model, where the data is normalised and data is split across multiple tables, with relationships captured using Ids. With MongoDB you can structure and store your data with the heirarchy simply contained in within the one document.
So in your example the Bookmark would not be a Document itself, but would be a sub document of the Account. Remove the #Document annotation from the Bookmark object, and the #DBRef annotations, and simply store the Bookmarks within the Account document.
This would give you a schema more like this:
{
"_id": 1,
"bookmarkSet": [
{
"uri": "http://www.foo.com",
"description": "foo"
},
{
"uri": "http://www.bar.com",
"description": "bar"
}
],
"username": "John",
"password": "password"
}
*Note: if you make the bookmarks sub documents you can remove the _id member from the Bookmark object
The best design will depend on how many bookmarks you expect each account to have. If its only a few bookmarks then what I suggested would work well. If you have thousands then you might want to structure it differently. There are lots of articles about schema design in NoSQL database. This one covers the options for embedding subdocuments quite well:
http://blog.mongodb.org/post/87200945828/6-rules-of-thumb-for-mongodb-schema-design-part-1
Related
I had a class like:
public class EmailAddress {
public String value;
public String tld() {...}
public String host() {...}
public String mailbox() {...}
}
Now I use this class in an Object / Entity:
#Entity
public class Customer {
public String name;
public EmailAddress mail;
}
Now, when I do a rest service for Customer, I get this format:
{
"id": 1,
"name": "Test",
"email": {
"value": "test#test.de"
}
}
But I only want "email": "test#test.de"
{
"id": 1,
"name": "Test",
"email": "test#test.de"
}
What I must do? I use Spring Boot and Hibernate Entities.
Thank you for any support
You should use DTO class in request handling and make mappings from DTO to Entity and backwards, e.g.:
public class CustomerDTO {
private Integer id;
private String name;
private String email;
}
You should use DataTransferObjects for your (REST) APIs.
The DTOs only contain the fields the interface should provide (or receive).
When receiving objects from the client and before returning the object from your Controller you can convert the DTOs to your domain model (Which could be your JPA entites classes).
Example for a controller method. We assume you get an object from an user-editor which contains all data you want to update in your database-objects and return the updated company DTO:
#PutMapping
public CustomerDto updateCustomer(CustomerEditorDto updatedCustomerDto) {
Customer updatedCustomer = CustomerConverter.convert(updatedCustomerDto);
updatedCustomer = customerService.updateCustomer(updatedCustomer);
return CustomerConverter.convert(updatedCustomer);
}
and your Converter class:
#NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CustomerConverter {
public static CustomerDto convert(Customer customer) {
CustomerDto result = null;
if (customer != null) {
// TODO: set fields in result-dto
}
return result;
}
public static Customer convert(CustomerEditorDto customer) {
Customer result = null;
if (customer != null) {
// TODO set fields in result;
}
return result;
}
}
and here are the DTOs
#Getter
#Setter
public class CustomerDto {
private Integer id;
private String name;
private String email;
}
#Getter
#Setter
public class CustomerEditorDto {
private Integer id;
private String firstName;
private String lastName;
private String email;
private String otherPropertyOrStuff;
}
This way you can separate the API modell from your JPA entites. You can use the same models for input/output. And you can even use a different model to work with inside your services and the finally convert them into your JPA entites, before persisting the data (or after reading the data).
There are tools which can take care of the conversion, like mapstruct.
* The above annotations #Getter, #Setter, ... are from project lombok and very are handy to generate boiler-plate code automatically.
I found an other easier solution, use a JsonSerializer on the entity Property:
#JsonSerialize(using = EmailAddressSerializer.class)
private EmailAddress email;
The serializer class:
public class EmailAddressSerializer extends StdSerializer<EmailAddress> {
public EmailAddressSerializer() {
super(EmailAddress.class);
}
protected EmailAddressSerializer(Class<EmailAddress> t) {
super(t);
}
#Override
public void serialize(EmailAddress email,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(email.value);
}
}
I am creating an application with embedded review documents inside Course documents with Spring Data Rest and MongoDB but I am unable to get reviews for a course. Here is my controller:
#Controller
#RequestMapping("/courses")
public class CourseController {
private final CourseRepository courseRepository;
public CourseController(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
#PatchMapping("/add-review")
public List<Review> addReview(#RequestBody AddReviewDto addReviewDto) {
Course course = courseRepository.findById(addReviewDto.getCourseId()).get();
Review review = new Review(new ObjectId().toString(), addReviewDto.getReview());
List<Review> reviews = course.getReviews();
reviews.add(review);
course.setReviews(reviews);
return courseRepository.save(course).getReviews();
}
#GetMapping("/{id}/reviews")
public List<Review> getAllReviewsForCourse(#PathVariable String id) {
Course course = courseRepository.findById(id).get();
return course.getReviews();
}
}
Here is the Course model:
#Getter
#Setter
#Document(collection = "courses")
#AllArgsConstructor
#NoArgsConstructor
public class Course {
public Course(#NotNull String code, #NotNull String name,
#NotNull String type, List<Review> reviews) {
this.code = code;
this.name = name;
this.type = type;
this.reviews = reviews;
}
#Id
private String id;
#NotNull
private String code;
#NotNull
private String name;
#NotNull
private String type;
private List<Review> reviews = new ArrayList<>();
}
And Review model:
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
public class Review {
#Id
private String id;
private String reviewText;
private String userName;
private String userId;
public Review(String id, Review other) {
this.id = id;
this.reviewText = other.reviewText;
this.userId = other.userId;
this.userName = other.userName;
}
}
When I send a request to http://localhost:8888/courses/605dc41f54beac4412cabadc, I successfully get reviews inside the course object as follows:
{
"code": "CS 101",
"name": "Introduction to Programming",
"type": "Lecture",
"reviews": [
{
"reviewText": "dfgsfgdgdg",
"userName": "yigit",
"userId": "604a9382777a83b08307c7e8"
}
]
}
But when I try to send the request to localhost:8888/courses/605dc41f54beac4412cabadc/reviews, I get 404 not found.
I debugged my code and seen that the code is running the correct controller, finding the course object and its reviews are visible in debugger but when I return course.getReviews(), it doesnt work.
You should be sending to http://localhost:8888/courses/605dc41f54beac4412cabadc/reviews
Try to open this URL in your browser
of course you get 404. Because your #RequestMapping is "/courses". try send the request to http://localhost:8888/courses/{id}/reviews
Turns out, I have used #Controller instead of #RestController...
I POST some data with Retrofit2 to a Spring Boot REST service and there are a lot of exceptions occurring in the server. This happens when I have relations in my database.
I have a REST service over a Spring Boot application that runs over the Heroku services, I was doing a login and a register tasks with an Android application, I am using Retrofit2 in Android to POST the data to the REST service, everything was working well until for some other reasons I create a relationship between users in my database, this relationship is a "follow", this is, create a relationship in a follow table where I have the ID of the user that is following and an ID of the user that is followed. When I create this relationship into the database and I try to login with the method that I created, I got a bunch of errors into the REST service that I do not know why is this happening.
So in Android I have the call of my Retrofit2 client and a method that creates the service passing as a parameter the UserService.class with the HTTP methods. I also pass as a parameter the user of the class User where have the information that I want to POST, then I call the enqueue method.
RetrofitClient.createService(UserService.class).login(user).enqueue(new CallBack<User>(){
#Override
public void onResponse(Call<User> call, Response<User> response) {
//Some logic here
}
});
Into my UserService.java I have the method that POST the user object information.
public interface UserService {
#POST("login")
public Call<User> login(#Body User user);
}
Now in the backend side I have a REST controller where I have the login endpoint that will be consumed for Retrofit2.
#PostMapping(path = "login", consumes = "application/json", produces = "application/json")
#CrossOrigin(origins = "*", methods= {RequestMethod.GET,RequestMethod.POST})
public Object login(#RequestBody String json) {
//Some logic here
}
As I said this endpoint runs fine when there are no relationships over a user into the DB, but when a user follow another one, this is, when there is a new row into the follow table, lets say:
follow table:
id_follow id_user_follower id_user_following
1 1 2
At the example above the user 1 follows the user 2, and when I try to login, this is, use the login method in the UserService class it throws me a bunch of errors.
com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)
com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(CollectionSerializer.java:145)
com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serialize(CollectionSerializer.java:107)
com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serialize(CollectionSerializer.java:25)
And this repeat over 300 lines of errors.
The thing here is that between the bunch of error the server return a 200 HTTP response, I managed the exceptions in Spring Boot and I catch that, when I catch I send a code for an error to my Android Retrofit2 client, but the login does not work.
Expected result:
After sending the POST from Retrofit2 to Spring Boot the response have to be a HTTP 200 response but no exceptions have to happen into the server.
Actual result:
There is a 200 HTTP response from the server but there are a lot of exceptions into the server that return an error code to the Android application and the login does not work.
This is the entity that I want to return as JSON from the RestController in Spring Boot.
#Entity
#Table(name = "users")
public class User extends AuditModel{
private static final long serialVersionUID = 1700575815607801150L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idUser;
private String name;
private String lastName;
#Column(name = "nick_name", unique = true)
private String nickName;
private String avatarResource;
#Column(unique=true)
private String email;
private String password;
private String birthDate;
private String gender;
private String postalCode;
private int active;
public Long getIdUser() {
return idUser;
}
public void setIdUser(Long idUser) {
this.idUser = idUser;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public String getAvatarResource() {
return avatarResource;
}
public void setAvatarResource(String avatarResource) {
this.avatarResource = avatarResource;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getBirthDate() {
return birthDate;
}
public void setBirthDate(String birthDate) {
this.birthDate = birthDate;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getPostalCode() {
return postalCode;
}
public void setPostalCode(String postalCode) {
this.postalCode = postalCode;
}
public Long getId() {
return idUser;
}
public void setId(Long idUser) {
this.idUser = idUser;
}
public int getActive() {
return active;
}
public void setActive(int active) {
this.active = active;
}
/* Relations */
#OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Comment> comments;
public List<Comment> getComments() {
return comments;
}
public void setComments(List<Comment> comments) {
this.comments = comments;
}
#OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<UserMemory> userMemories;
public List<UserMemory> getUserMemories() {
return userMemories;
}
public void setUserMemories(List<UserMemory> userMemories) {
this.userMemories = userMemories;
}
#OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Vote> votes;
public List<Vote> getVotes() {
return votes;
}
public void setVotes(List<Vote> votes) {
this.votes = votes;
}
#OneToMany(mappedBy = "userFollower", fetch = FetchType.LAZY)
private List<Follow> usersFollowers;
public List<Follow> getUsersFollowers() {
return usersFollowers;
}
public void setUsersFollowers(List<Follow> usersFollowers) {
this.usersFollowers = usersFollowers;
}
#OneToMany(mappedBy = "userFollowing", fetch = FetchType.LAZY)
private List<Follow> usersFollowing;
public List<Follow> getUsersFollowing() {
return usersFollowing;
}
public void setUsersFollowing(List<Follow> usersFollowing) {
this.usersFollowing = usersFollowing;
}
}
By having fetchtype.LAZY, some of the values won't exist during serialization. This will make the ObjectMapper to try to fetch these and it all will end up in some kind of infinite loop.
It is never recommended to serialize #Entity annotated classes because database tables can change and that in turn will change the API for the calling clients.
Best way is to have specific ResponseDTOs that you transfer your data to before serialization so that the API and the database tables can change without breaking anything.
So, the problem was that in the backend I was using the writeValueAsString of the ObjectMapper class like this.
public Object register(#RequestBody String json){
User user = new User();
user.set...
...
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(user);
}
For some reason the writeValueAsString method gives a StackOverFlowException for a recursive behavior when there is a relationship into the database, this is a problem related with JPA or Hibernate, I am not sure about which one of those.
The solution was to write my own method to build a JSONObject with the information of my POJO:
public String buildUserJSON(User user) {
JSONObject userJson = new JSONObject();
userJson.put("idUser", user.getIdUser());
...
return userJson.toString();
}
And then call this method in my RestController to build the JSON that I want to return. As I said I do not know what was the problem but at least this was the solution for me.
Note: I found the solution to this be cause in the past I was with a problem similar like this one but when I was trying to return a List<T> as a JSON, so, I though that was related with the same thing.
Regards.
The problem was with Jackson, so to get out of this problem you most use two annotations in the relations of your entities.
More information, please see the next link: Here is the answer
Hope it can help for anyone.
I am trying to add HATEOAS links with Resource<>, while also filtering with #JsonView. However, I don't know how to add the links to nested objects.
In the project on on Github, I've expanded on this project (adding in the open pull request to make it work without nested resources), adding the "Character" entity which has a nested User.
When accessing the ~/characters/resource-filtered route, it is expected that the nested User "player" appear with the firstNm and bioDetails fields, and with Spring generated links to itself, but without the userId and lastNm fields.
I have the filtering working correctly, but I cannot find an example of nested resources which fits with the ResourceAssembler paradigm. It appears to be necessary to use a ResourceAssembler to make #JsonView work.
Any help reconciling these two concepts would be appreciated. If you can crack it entirely, consider sending me a pull request.
User.java
//package and imports
...
public class User implements Serializable {
#JsonView(UserView.Detail.class)
private Long userId;
#JsonView({ UserView.Summary.class, CharacterView.Summary.class })
private String bioDetails;
#JsonView({ UserView.Summary.class, CharacterView.Summary.class })
private String firstNm;
#JsonView({ UserView.Detail.class, CharacterView.Detail.class })
private String lastNm;
public User(Long userId, String firstNm, String lastNm) {
this.userId = userId;
this.firstNm = firstNm;
this.lastNm = lastNm;
}
public User(Long userId) {
this.userId = userId;
}
...
// getters and setters
...
}
CharacterModel.java
//package and imports
...
#Entity
public class CharacterModel implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView(CharacterView.Summary.class)
private Long characterId;
#JsonView(CharacterView.Detail.class)
private String biography;
#JsonView(CharacterView.Summary.class)
private String name;
#JsonView(CharacterView.Summary.class)
private User player;
public CharacterModel(Long characterId, String name, String biography, User player) {
this.characterId = characterId;
this.name = name;
this.biography = biography;
this.player = player;
}
public CharacterModel(Long characterId) {
this.characterId = characterId;
}
...
// getters and setters
...
}
CharacterController.java
//package and imports
...
#RestController
#RequestMapping("/characters")
public class CharacterController {
#Autowired
private CharacterResourceAssembler characterResourceAssembler;
...
#JsonView(CharacterView.Summary.class)
#RequestMapping(value = "/resource-filtered", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.OK)
public Resource<CharacterModel> getFilteredCharacterWithResource() {
CharacterModel model = new CharacterModel(1L, "TEST NAME", "TEST BIOGRAPHY", new User(1L, "Fred", "Flintstone"));
return characterResourceAssembler.toResource(model);
}
...
}
CharacterResourceAssembler.java
//package and imports
...
#Component
public class CharacterResourceAssembler implements ResourceAssembler<CharacterModel, Resource<CharacterModel>>{
#Override
public Resource<CharacterModel> toResource(CharacterModel user) {
Resource<CharacterModel> resource = new Resource<CharacterModel>(user);
resource.add(linkTo(CharacterController.class).withSelfRel());
return resource;
}
}
So far I am using it like below to get data from multiple tables:
#RequestMapping(path = "/get_data_on_login", method = RequestMethod.GET)
public ResponseEntity get_data_on_login(#RequestParam(value="username") String username) throws Exception {
List<TopicBean> topics = topicService.findAllTopics();
UserInfo role = userInfoService.findRoleByUsername(username);
return new ResponseEntity(new LoginDataBean( topics, role.getRole(), role.getUserImage()), HttpStatus.OK);
}
but as the requirement to get data from more number of table increases, its getting complicated.
What I am looking for is to get data like this but I am not sure if this is correct:
public interface LoginRepository extends JpaRepository<LoginDataBean, Long>{
List<LoginDataBean> SearchByTopicBeanAndCommentBeanAndCommentLikes(TopicBean topicBean, CommentBean commentBean, CommentLikes commentLikes);
}
I have LoginDataBean like this:
public class LoginDataBean {
#Autowired
private final List<TopicBean> topics;
private String userImage;
private String role;
public LoginDataBean(List<TopicBean> topics, String role, String userImage){
this.topics = topics;
this.role = role;
this.userImage = userImage;
}
public String getRole(){
return role;
}
public String userImage(){
return userImage;
}
public List<TopicBean> getTopics() {
return topics;
}
}