bidirectional relationship returning empty set on OneToMany and works only on ManyToOne - spring-boot

i have 2 entities, Category and Feature, each Category has one or many features.
when creating a category along with its features, fetching the new category returns an empty set on the features attribute.
#PostMapping("/sub")
#PreAuthorize("hasRole('ROLE_admin')")
public HttpEntity<CategoryDTO> createSubCategory(#Valid #RequestBody CreateCategory createCategory)
{
Category category = categoryService.create(createCategory,mainCategoryService.one(createCategory.getMainCategory_id()));
featureService.bulkCreate(createCategory.getFeatures(),category);
return ResponseEntity.status(HttpStatus.CREATED).body(modelMapper.map(category,CategoryDTO.class));
}
this is the data that i'm sending:
{"name":"SUB","mainCategory_id":1,"features":{"F1":"SLIDER","F2":"CHECKBOX"}}
and this is the data returned by the controller:
{"id":2,"name":"SUB","mainCategory":{"id":1,"name":"CATEGORY"},"features":[]}
as you can see, the features are empty.
This is the test to create a category with its features:
#Test
public void testIfAdminCanCreateSubCategory_expect201AndMainCategoryIdEqualsTheAssociatedOne() throws Exception {
String category = "{\"name\" : \"CATEGORY\"}";
String sub = "{\"name\":\"SUB\",\"mainCategory_id\":1,\"features\":{\"F1\":\"SLIDER\",\"F2\":\"CHECKBOX\"}}";
mockMvc().with(keycloakAuthenticationToken().authorities("ROLE_admin")).perform(post("/categories").content(category).contentType("application/json"))
.andDo(print())
.andDo(r -> mockMvc().with(keycloakAuthenticationToken().authorities("ROLE_admin"))
.post(sub,"/categories/sub")
.andExpect(status().isCreated())
.andExpect(jsonPath("$.mainCategory.id").value(1))
.andDo(print())
// .andExpect(jsonPath("$.features[0]").value("SLIDER"))
);
//featureService.all().forEach( f -> System.out.println(f.getCategory().getId()));
}
when decommenting the last line, it prints the category id ( which is 2 as shown in the returned data ), meaning that the ManyToOne is working, but not the OneToMany.
My models:
#Entity
#EntityListeners( AuditingEntityListener.class )
#Data
public class Category {
#Id #GeneratedValue(strategy=GenerationType.AUTO) private Long id;
....
#OneToMany(cascade = CascadeType.ALL, mappedBy = "category" , fetch = FetchType.LAZY)
Set<Feature> features = new HashSet<>();
}
#Entity
#Data
public class Feature {
#Id #GeneratedValue private Long id;
private String name;
private FeatureType type;
#ManyToOne Category category;
}
the create method in categoryService:
#Override
public Category create(CreateCategory createCategory, MainCategory mainCategory) {
Category category = new Category();
category.setName(createCategory.getName().toUpperCase());
category.setMainCategory(mainCategory);
return categoryRepository.save(category);
}
the bulkCreate method in the featureService:
#Override
public Feature create(String feature, FeatureType type, Category category) {
Feature f = new Feature();
f.setName(feature);
f.setType(type);
f.setCategory(category);
return featureRepository.save(f);
}
#Override
public void bulkCreate(Map<String, FeatureType> features, Category category) {
features.forEach( (name,type) -> create(name,type,category));
}
myDTOs:
#Data
public class CategoryDTO {
private Long id;
private String name;
private MainCategoryDTO mainCategory;
private Set<FeatureDTO> features;
}
#Data
public class FeatureDTO {
private Long id;
private String name;
private FeatureType type;
}
#Data
public class MainCategoryDTO {
private Long id;
private String name;
}
EDIT 1 :
i've added a method on my categoryService that sets the category features.
#Override
public Category addFeatures(Category category, List<Feature> features) {
category.setFeatures(features);
return categoryRepository.save(category);
}
and on my controller, i added the commented line so i can associate features to the category
#PostMapping("/sub")
#PreAuthorize("hasRole('ROLE_admin')")
public HttpEntity<CategoryDTO> createSubCategory(#Valid #RequestBody CreateCategory createCategory)
{
Category category = categoryService.create(createCategory,mainCategoryService.one(createCategory.getMainCategory_id()));
#category = categoryService.addFeatures(category,featureService.bulkCreate(createCategory.getFeatures(),category));
return ResponseEntity.status(HttpStatus.CREATED).body(modelMapper.map(category,CategoryDTO.class));
}

Related

I have a problem with "You may also like" feature in Spring boot JPA

I want to create a simple "You may also like" feature for a blog.
There are posts and each of them has one or more tags. Also a tag can contain many posts. I want to implement the feature where you open a post and the posts, which have similar tags, are recommended to you.
So i created 3 entities:
Post.java
#Entity
#Table
public class Post {
#Id
#Column
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String text;
#Column
private String author;
#OneToMany(mappedBy = "post")
Set<PostTags> postTags;
public Post(){}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Set<PostTags> getPostTags() {
return postTags;
}
public void setPostTags(Set<PostTags> postTags) {
this.postTags = postTags;
}
}
Tags.java
#Entity
#Table
public class Tags {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column
private Long id;
#Column
private String name;
#OneToMany(mappedBy = "tag")
Set<PostTags> postTags;
public Set<PostTags> getPostTags() {
return postTags;
}
public void setPostTags(Set<PostTags> postTags) {
this.postTags = postTags;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
PostTags.java
#Entity
public class PostTags {
#Id
#GeneratedValue (strategy = GenerationType.IDENTITY)
#Column
private Long id;
#ManyToOne
#JoinColumn(name = "post_id")
private Post post;
#ManyToOne
#JoinColumn(name = "tag_id")
private Tags tag;
public PostTags(){}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Post getPost() {
return post;
}
public void setPost(Post post) {
this.post = post;
}
public Tags getTag() {
return tag;
}
public void setTag(Tags tag) {
this.tag = tag;
}
}
And repositories:
#Repository
public interface PostTagsRepository extends JpaRepository<PostTags, Long> {
#Query("select p.post from PostTags p where p.tag.id IN :tagIds")
Set<Post> findPostsbyTagIds (List<Long> tagIds);
}
#Repository
public interface PostRepository extends JpaRepository<Post, Long> {
#Query("select p from Post p where p.author = :author")
Set<Post> findPostsByAuthor(String author);
}
I managed to create this feature in an amateurish way, but better than nothing. I piled up everything in one method just to test it:
#GetMapping("/posts")
public Set<Post> showRecommendedPosts(){
//Imitate post id
long postId = 1;
Post postFound = postRepository.findById(postId).get();
Set<PostTags> postTags = postFound.getPostTags();
List<Long> listTagIds = new ArrayList<>();
//extract ids of the tags from the post
for(PostTags tag : postTags){
listTagIds.add(tag.getTag().getId());
}
//find posts by Author
Set<Post> postsByAuthor = postRepository.findPostsByAuthor(postFound.getAuthor());
//find posts by Tags
Set<Post> postsByTagIds = postTagsRepository.findPostsbyTagIds(listTagIds);
//We combine both sets
Set<Post> recommendedPosts = new HashSet<>(postsByAuthor);
recommendedPosts.addAll(postsByTagIds);
recommendedPosts.remove(postFound);
return recommendedPosts;
}
But this works only if i manually add data to "post_tags" table in the db like this:
Here is my question, i don't know how to add multiple tags to a post in Spring. Because it would be something like this:
PostTags newPostTag1 = new PostTags();
newPostTag.setPost(post1);
newPostTag.setTag(tag1);
PostTags newPostTag2 = new PostTags();
newPostTag2.setPost(post1);
newPostTag2.setTag(tag2);
PostTags newPostTag3 = new PostTags();
newPostTag3.setPost(post1);
newPostTag3.setTag(tag3);
And so on...
Therefore, it's not an option. So how can i save tags correctly? Or have my entities been created incorrectly? What is my mistake? Thank you!
I am not sure to understand the idea behind Tag being an Entity.
How I see it is you use the postTags and change it to tags. This tags would be a Set of an enum if you want to restrict the user or a Set of String other way. After that, I would add an endpoint that return post based on a tag or a list of tags for your You may also like feature. This endpoint just make a request to the database (find posts where tags contains givenTag max 10). At the end, you only have one entity :
#Entity
#Table
public class Post {
#Id
#Column
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String text;
#Column
private String author;
#Column
#Convert(converter = StringListConverter.class)
Set<String> tags;
// ...
}
Converter implementation here

Spring hibernate ignore json object

I need to remove cart object from json, but only in one controller method and that is:
#GetMapping("/users")
public List<User> getUsers() {
return userRepository.findAll();
}
User
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#NotBlank(message = "Name cannot be empty")
private String name;
#OneToOne
private Cart cart;
}
Cart
#Entity
public class Cart {
#Id
private String id = UUID.randomUUID().toString();
#OneToMany
private List<CartItem> cartItems = new ArrayList<>();
#OneToOne
#JsonIgnore
#OnDelete(action = OnDeleteAction.CASCADE)
private User user;
}
I have done it with simple solution so i loop trough all users, and set their cart to null,and then anotated user entity with #JsonInclude(JsonInclude.Include.NON_NULL)
But i dont think this is propper solution, so im searching for some better solution..
How am i able to do this?
Thanks...
You can create DTO (data transfer object) class like this:
#Data
public class UsersDto {
private Integer id;
private String name;
public UsersDto(User user) {
this.id = user.id;
this.name= user.name;
}
}
and than create List<UsersDto>
#GetMapping("/users")
public List<UsersDto> getUsers() {
List<User> users = userRepository.findAll();
return users
.stream()
.map(o -> new UsersDto(o))
.collect(Collectors.toList());
}
You should use Data Projection.
In your use case, you can use an interface projection:
public interface CartlessUser {
Integer getId();
String getName();
}
And In your repository:
public interface UserRepository extends JpaRepository<User, Integer> {
List<CartlessUser> findAllBy();
}
The interface projection will help generate the sql query for only selecting the id, name fields. This will save you from fetching the Cart data when you're just going to throw it away anyways.

I don't know why the double values are displayed in postman. Is the my code correct?

This is my Book class:
#Entity
#Table(name="book")
public class Book {
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
#ManyToOne(targetEntity=Category.class,cascade=CascadeType.ALL,fetch=FetchType.LAZY)
#JoinColumn(name="CategoryId")
public Category category;
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column(length=10)
private int book_id;
#Column(length=128)
private String title;
#Column(length=64)
private String author;
#Column(length=200)
private String description;
#Column(length=10)
private int ISBN;
#Column(length=10)
private float price;
private Date published_Date;
#Lob
#Column
#Basic(fetch = FetchType.LAZY)
private byte[] icon;
//getter and setter
}
This is my Category class:
#Entity
#Table(name="category1")
public class Category {
#Id
#Column(length=12)
#GeneratedValue(strategy=GenerationType.AUTO)
public int CategoryId;
#Column(length=50)
public String CategoryName;
//#JsonBackReference
#OneToMany(mappedBy="category")
private List<Book> books = new ArrayList<Book>();
//getter and setter
}
The relationship between them is one to many.
This is my Category Service class
#Service
#Transactional
public class AdminServiceImpl implements AdminService {
#Autowired
private CategoryDao dao;
#Autowired
private BookDao dao1;
#Override
public List<Category> getAllCategory(){
return dao.findAll();
}
}
My Controller class
#RestController
#RequestMapping("/bookstore")
public class CategoryController {
#Autowired
private AdminService service;
#GetMapping("/GetAllCategory")
private ResponseEntity<List<Category>> getAllCategory() {
List<Category> catlist = service.getAllCategory();
return new ResponseEntity<List<Category>>(catlist, new HttpHeaders(), HttpStatus.OK);
}
}
My category table already has data.When i try to display them it is showing double values.
Displaying values using Postman
The Category table in the Database: Database table
Jackson's ObjectMapper uses the Java bean pattern and it expects the following
public class Foo {
public Object bar;
public Object getBar() {...}
public void setBar(Object bar) {...}
}
The getters and setters start with get and set, respectively, followed by the corresponding field name with its first letter capitalized.
Change
CategoryId to categoryId (first letter lowercase)
and
CategoryName to categoryName

Spring JPARepository querying many to many intersection table

I have 3 entity classes as follows (Example taken from https://hellokoding.com/jpa-many-to-many-extra-columns-relationship-mapping-example-with-spring-boot-maven-and-mysql/)
Book class
#Entity
public class Book{
private int id;
private String name;
private Set<BookPublisher> bookPublishers;
public Book() {
}
public Book(String name) {
this.name = name;
bookPublishers = new HashSet<>();
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#OneToMany(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true)
public Set<BookPublisher> getBookPublishers() {
return bookPublishers;
}
public void setBookPublishers(Set<BookPublisher> bookPublishers) {
this.bookPublishers = bookPublishers;
}
}
Publisher class
#Entity
public class Publisher {
private int id;
private String name;
private Set<BookPublisher> bookPublishers;
public Publisher(){
}
public Publisher(String name){
this.name = name;
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#OneToMany(mappedBy = "publisher")
public Set<BookPublisher> getBookPublishers() {
return bookPublishers;
}
public void setBookPublishers(Set<BookPublisher> bookPublishers) {
this.bookPublishers = bookPublishers;
}
}
Intersection Table
#Entity
#Table(name = "book_publisher")
public class BookPublisher implements Serializable{
private Book book;
private Publisher publisher;
private Date publishedDate;
#Id
#ManyToOne
#JoinColumn(name = "book_id")
public Book getBook() {
return book;
}
public void setBook(Book book) {
this.book = book;
}
#Id
#ManyToOne
#JoinColumn(name = "publisher_id")
public Publisher getPublisher() {
return publisher;
}
public void setPublisher(Publisher publisher) {
this.publisher = publisher;
}
#Column(name = "published_date")
public Date getPublishedDate() {
return publishedDate;
}
public void setPublishedDate(Date publishedDate) {
this.publishedDate = publishedDate;
}
}
I want to query 2 things,
Get list of books belonging to a particular publisher e.g. get all books associated with publisher 100
Get list of books not associated with a particular publisher e.g. get all books not associated with publisher 100
I want to achieve this using a simple JPARepository query if possible like findByXYZIn(...) etc.
Please let me know if querying a many to many relation is possible using JPA repository queries and if yes, whether I can do it directly or would it require any changes in the entity classes
In BookRepository
Get publisher's books
findBooksByBookPublishersPublisherId(Long publisherId)
Get books not published by publisher
findBooksByBookPublishersPublisherIdNot(Long publisherId)
IMHO Publication is much more apropriate name then BookPublisher in your case as Publisher by itself could be BookPublisher (a published that publishing books)
I'm not sure if you can make it just by method name. But you definitely can use JPA query. Something like this: "SELECT b FROM Book b JOIN b.bookPublishers bp JOIN bp.publisher p WHERE p.id = ?1". and with not equal for the second case
Well you can use named Queries to fulfill your requirements:
#Query("select b from Book b where b.publisher.idd = ?1")
Book findByPublisherId(int id);
#Query("select b from Book b where b.publisher.idd <> ?1")
Book findByDifferentPublisherId(int id);
Take a look at Using #Query Spring docs for further details.

JPA - Spring boot -#OneToMany persistence works fine but i get a strange object when returning Json object

I have two entities ( Category | product ) with #OneToMany bidirectional relationship.
#Entity
public class Category {
public Category() {
// TODO Auto-generated constructor stub
}
public Category(String name,String description) {
this.name=name;
this.description=description;
}
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private long cid;
private String name;
private String description;
#OneToMany(mappedBy="category",cascade=CascadeType.ALL, orphanRemoval=true)
private Set<Product> products;
/..getters and setter.../
}
#Entity
public class Product {
public Product() {
// TODO Auto-generated constructor stub
}
public Product(long price, String description, String name) {
// TODO Auto-generated constructor stub
this.name=name;
this.description=description;
this.price=price;
}
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private long pid;
private long price;
private String name;
private String description;
#ManyToOne
private Category category;
/..getters and setters../
}
in my controller I have a function /categoris that add a new category with one product, it works great and in my database I've got a foreign category id
But when i try to retrieve all the categories with responseBody i got a strange object exactely in category ( i want have in product, category : the category id instead of the object it's self )
public #ResponseBody Category create() {
Category c=new Category("LIGA","messi feghouli cristiano");
Product p=new Product(200,"jahd besaf","Real Madrid");
if(c.getProducts()!=null){
c.addProducts(p);
}else{
Set<Product> products=new HashSet<Product>();
products.add(p);
c.setProducts(products);
}
p.setCategory(c);
cDao.save(c); pDao.save(p);
return c;
}
#RequestMapping(value="/categories",method = RequestMethod.GET)
public #ResponseBody List<Category> categories() {
return cDao.findAll();
}
this is the strage object that i got :
{"cid":1,"name":"LIGA","description":"messi feghouli cristiano","products":[{"price":200,"name":"Real Madrid","description":"jahd besaf","category":{"cid":1,"name":"LIGA","description":"messi feghouli cristiano","products":[{"price":200,"name":"Real Madrid","description":"jahd besaf","category":{"cid":1,"name":"LIGA","description":"messi feghouli cristiano","products":[{"price":200,"name":"Real Madrid","description":"jahd besaf","category":{"cid":1,"name":"LIGA","description":"messi feghouli cristiano","products":
That's exactly as it should be.
If you wish to avoid a circular reference, use the #JsonBackReference annotation. This prevents Jackson (assuming you're using Jackson) from going into an infinite loop and blowing your stack.
If you want the ID instead of the entity details, then create getProductID & getCategoryID methods and annotate the entity accessor with #JsonIgnore.

Resources