I am new to Spring Boot, Jackson, and JPA. I am attempting to build an RESTful application. From my understanding of the Spring Boot documentation, much of the work of deserializing data and returning JSON is "magically" handled by the Spring framework (when the appropriate starter/dependencies included).
My Controller endpoint is simple.
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.midamcorp.roominspections.models.*;
import com.midamcorp.roominspections.service.ItemService;
#RestController
#RequestMapping("/item")
public class ItemController {
#Autowired
private ItemService itemService;
#RequestMapping(method= RequestMethod.GET)
public List<Item> getItems() {
return itemService.findAll();
}
#RequestMapping(method=RequestMethod.GET, value="/{id}")
public Item getItem(#PathVariable int id) {
return itemService.findOne(id);
}
#RequestMapping(method=RequestMethod.GET,value="/page/{pageNumber}")
public List<Item> getItemsByPage(#PathVariable int pageNumber) {
return itemService.findByPage(pageNumber);
}
}
However, my classes (JPA entities) exhibit some relationships. In example:
import java.util.Set;
import javax.persistence.*;
#Entity
#Table(name="Items")
public class Item {
public Item() {}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int ItemID;
#ManyToOne(optional=false)
#JoinColumn(name="CommentCategoryID", insertable = false, updatable = false)
private CommentCategory commentCategory;
#ManyToOne(optional=false)
#JoinColumn(name="ItemCategoryID", insertable = false, updatable = false)
private ItemCategory itemCategory;
#OneToMany(mappedBy="item",targetEntity=ReportDetail.class)
private Set<ReportDetail> detail;
private String ItemName;
private String ItemDescription;
private String ItemType;
#Column(name="PageNumber")
private int pageNumber;
private int PossPoints;
private int CommentCategoryID;
private int ItemCategoryID;
// getters and setters follow
}
Whenever, I attempt to access the item endpoint, I receive the following error:
Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: could not deserialize; nested exception is com.fasterxml.jackson.databind.JsonMappingException: could not deserialize (through reference chain: java.util.ArrayList[0]->com.midamcorp.roominspections.models.Item["commentCategory"]->com.midamcorp.roominspections.models.CommentCategory["comments"]->org.hibernate.collection.internal.PersistentSet[0]->com.midamcorp.roominspections.models.Comment["reportDetails"])
Now I understand the issue lies in deserializing the data (through Jackson), and the problem "ends" with the reportDetails property of the Comment entity. However, I have inspected both the Comment and ReportDetails entities and could find nothing amiss.
Comment
import java.util.Set;
import javax.persistence.*;
#Entity
#Table(name="Comments")
public class Comment {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int CommentID;
public Comment() {}
#OneToMany(mappedBy="comment", targetEntity=ReportDetail.class)
private Set<ReportDetail> reportDetails;
#ManyToOne(optional=false)
#JoinColumn(name="CommentCategoryID", insertable = false, updatable = false)
private CommentCategory commentCategory;
private int CommentCategoryID;
private String CommentBody;
// getters and setters
}
ReportDetail
import javax.persistence.*;
#Entity
#Table(name="ReportDetailRows")
public class ReportDetail {
public ReportDetail() {}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int ReportRowID;
private int ReportID;
private int ItemID;
private int CommentID;
private int PointsDeducted;
private String ItemIMG;
#ManyToOne(optional=false)
#JoinColumn(name="CommentID", insertable = false, updatable = false)
private Comment comment;
#ManyToOne(optional=false)
#JoinColumn(name="ReportID", insertable = false, updatable = false)
private ReportSummary summary;
#ManyToOne(optional=false)
#JoinColumn(name="ItemID", insertable = false, updatable = false)
private Item item;
// getters and setters
}
Repository interfaces (extending the CrudRepository) and a Service layer are used.
The following is an example service:
import java.util.List;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.midamcorp.roominspections.models.Item;
import com.midamcorp.roominspections.models.ItemRepo;
#Service
#Transactional
public class ItemServiceImpl implements ItemService {
#Autowired
private ItemRepo itemRepo;
#Override
public List<Item> findAll() {
return (List<Item>) itemRepo.findAll();
}
#Override
public List<Item> findByPage(int pageNumber) {
return itemRepo.findByPageNumber(pageNumber);
}
#Override
public Item findOne(int id) {
return itemRepo.findOne(id);
}
}
In my experience with .NET, I would have to create DTOs (using a library such as AutoMapper) to flatten the entities for transfer. However, from my review of the Jackson documentation, it seemed as if the library handles such flattening automatically (note, I realize it is not a good practice to transfer such large volumes of data; as I am new to the technologies, I simply want to get it working first) . If DTOs are required, where would they be situated? For example, in the Controller, Service, Repository, etc?
I truly appreciate any advice. I have searched around but have been unable to find a solution for my particular problem.
Related
I created POST API in Spring Boot, but 500 error occurs.
"timestamp": "2023-01-27T16:27:32.609+00:00",
"status": 500,
"error": "Internal Server Error",
"trace": "org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["PRIMARY KEY ON PUBLIC.CATEGORY(CATEGORY_ID) ( /* key:1 */ 1, U&'\\c1fc\\d551\\bab0', 1)"; SQL statement:\ninsert into category (category_id, category_name, site_user_id) values (default, ?, ?)
I want to put data in the 'category' table with 'categoryId', 'category_name', and 'site_user_id' as columns through POST API. It seems to be caused by putting 'siteUser' entity instead of 'site_user_id', but I don't know how to modify the code.
Below is the code I wrote.
Category.java
package com.kakaotrack.choco.linkupapi.category;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.kakaotrack.choco.linkupapi.linkcollection.LinkCollection;
import com.kakaotrack.choco.linkupapi.user.SiteUser;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
#Entity
#Data
#Table(name = "category")
#NoArgsConstructor
public class Category {
public Category(String category_name, SiteUser siteUser){
this.category_name = category_name;
this.siteUser = siteUser;
}
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int categoryId;
private String category_name;
#OneToMany(mappedBy = "category", cascade = CascadeType.REMOVE)
#JsonIgnoreProperties({"category"})
private List<LinkCollection> link_collection_list;
#ManyToOne
private SiteUser siteUser;
}
SiteUser.java
package com.kakaotrack.choco.linkupapi.user;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
#Getter
#Setter
#Entity
#Table(name = "users")
#NoArgsConstructor
public class SiteUser {
public SiteUser(String username, String email){
this.username=username;
this.email=email;
}
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(unique = true)
private String username;
private String password;
#Column(unique = true)
private String email;
}
CategoryService.java
package com.kakaotrack.choco.linkupapi.category;
import com.kakaotrack.choco.linkupapi.linkcollection.LinkCollection;
import com.kakaotrack.choco.linkupapi.user.SiteUser;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
#RequiredArgsConstructor
#Service
public class CategoryService {
private final CategoryRepository categoryRepository;
public List<Category> getAll() {return categoryRepository.findAll();}
public List<Category> getBySiteUser(int id){
return categoryRepository.findBySiteUserId(id);
}
public Category createCategory(String categoryName, SiteUser siteUser){
Category category = new Category(categoryName, siteUser);
return categoryRepository.save(category);
}
public void deleteByCategoryId(int category_id){categoryRepository.deleteById(category_id);}
}
CategoryController.java
package com.kakaotrack.choco.linkupapi.category;
import com.kakaotrack.choco.linkupapi.linkcollection.LinkCollection;
import com.kakaotrack.choco.linkupapi.linkcollection.LinkCollectionRepository;
import com.kakaotrack.choco.linkupapi.user.SiteUser;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
#RestController
#RequiredArgsConstructor
public class CategoryController {
private final CategoryService categoryService;
#GetMapping(value = "/categories")
public List<Category> getAll() {return categoryService.getAll();}
#GetMapping(value = "/categories/{id}")
public List<Category> getBySiteUser(#PathVariable int id) {return categoryService.getBySiteUser(id);}
#PostMapping(value = "/categories")
public Category createCategory(String categoryName, SiteUser siteUser){
Category category = categoryService.createCategory(categoryName, siteUser);
return category;
}
#DeleteMapping(value = "/categories/{category_id}")
public void deleteCategory(#PathVariable int category_id){ categoryService.deleteByCategoryId(category_id);}
}
DELETE and GET APIs work well.
Try to update SiteUser fields as shown below:
#ManyToOne(optional = true, fetch = FetchType.LAZY)
#JoinColumn(name = "site_user_id", referencedColumnName = "id")
private SiteUser siteUser;
I think the issue is with the category_name. It is not following the standard naming convention. Underscore is used to separate property names in JPA custom methods.
#Column(name = "category_name")
private String categoryName;
NB: Also you have to implement the changes mentioned by Murat. Use optional = false if it is Not Null in DB
When trying to send a request, with the same "flower_id", to Postman, returns 500 with message:
"could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement."
At the same time, it does not matter if the same ids are in the same request or in different ones with different users, if one flower has already been added earlier, it is no longer possible to add it to another user.
Entity Order:
import javax.persistence.*;
import java.time.LocalDate;
import java.util.List;
#Entity
#Table(name = "orders")
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private LocalDate orderCreateDate;
private LocalDate orderCompleteDate;
#ManyToOne(fetch = FetchType.LAZY)
private User user;
#ManyToMany
private List<Flower> flower;
private Integer price;
public Order() {
}
public Order(LocalDate orderCreateDate, LocalDate orderCompleteDate, User user, List<Flower> flower) {
this.orderCreateDate = orderCreateDate;
this.orderCompleteDate = orderCompleteDate;
this.user = user;
this.flower = flower;
}
//Getters and setters
}
Entity Flower:
import javax.persistence.*;
#Entity
#Table(name = "flowers")
public class Flower {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer price;
public Flower() {
}
public Flower (String name, Integer price) {
this.name = name;
this.price = price;
}
//Getters and Setters
}
OrderService:
import com.learning.flowershop.Entity.Order;
import com.learning.flowershop.Repositories.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.List;
#Service
public class OrderService {
private final OrderRepository orderRepository;
#Autowired
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public List<Order> getAllOrdersByUserId(Long userId) {
return orderRepository.findAllByUserId(userId);
}
#Transactional
public void saveOrder(Order order) {
orderRepository.save(order);
}
}
Did you check the constraints in your database? The 500 error indicates an internal server error. It seems like there might be a unique constraint in your relation table which causes an SQL exception. If this exception is not properly caught it will get rethrown as an internal server error.
I still don't fully understand why this is the case, but I still want to leave a solution to my question.
It was only worth adding a save method for User
public User saveUser(User user) {
return userRepository.save(user);
}
Which is strange, because before that all my users were quietly saved.
I have a simple question about JpaRepository.
First, this is my Entity class.
package com.surveypedia.domain.pointhistory;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
#NoArgsConstructor
#Getter
#Entity
#Table(name = "pointhistory")
public class PointHistory {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer ph_code;
#Column(nullable = false)
private String email;
#Column(nullable = false, name = "s_code")
private Integer s_code;
#Column(nullable = false)
private Integer pointchange;
#Column(nullable = false)
#Enumerated(EnumType.STRING)
private PointHistoryType ph_type;
public PointHistory(String email, Integer s_code, Integer pointchange, PointHistoryType ph_type) {
this.email = email;
this.s_code = s_code;
this.pointchange = pointchange;
this.ph_type = ph_type;
}
}
And below is my repository interface to do CRUD operations.
package com.surveypedia.domain.pointhistory;
import com.surveypedia.tools.SQL;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface PointHistoryRepository extends JpaRepository<PointHistory, Integer> {
List<PointHistory> findByEmail(String email);
PointHistory findByS_codeAndEmailAndPh_type(Integer s_code, String email, PointHistoryType ph_type);
}
After starting my spring-boot project, I get this error :
java.lang.IllegalArgumentException: Failed to create query for method public abstract com.surveypedia.domain.pointhistory.PointHistory com.surveypedia.domain.pointhistory.PointHistoryRepository.findByS_codeAndEmailAndPh_type(java.lang.Integer,java.lang.String,com.surveypedia.domain.pointhistory.PointHistoryType)! No property s found for type PointHistory!
I tried findByEmailAndS_codeAndPh_type with proper arguments, but I got the same error log. What's the problem with my method there?
The problem is that underscore (_) is restricted to class hierarchies in spring-data-jpa mathod names. It's based on the simple convention of using camelCase in Java, which you're breaking.
Rename the field ph_code to phCode and s_code to sCode both in the entity and in the method name.
I am using Spring Boot, and have the following Entity definitions (abridged):
package com.vw.asa.entities;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.PreUpdate;
import javax.validation.constraints.NotNull;
public abstract class CmsModel extends Model {
#Basic(optional = false)
#NotNull
#Column(name = "is_active")
private short isActive;
public short getIsActive() {
return isActive;
}
public void setIsActive(short isActive) {
this.isActive = isActive;
}
public void setIsActive(String isActive) {
if (isActive.equals("true")) {
this.isActive = IS_TRUE;
} else if (isActive.equals("1")) {
this.isActive = IS_TRUE;
} else {
this.isActive = IS_FALSE;
}
}
}
Then I have several models which extend this 'base' model, following this flavor:
package com.vw.asa.entities.cms;
import com.vw.asa.entities.CmsModel;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Date;
import java.util.List;
/**
* #author Barry Chapman
*/
#Entity
#Table(name = "cms_extra_questions", schema = "asa")
public class CmsExtraQuestions extends CmsModel {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "id")
private Integer id;
...
}
When I initialize an instance of CmsExtraQuestions as a result of a hibernate query, if I call setActive(true) on the object, it has no effect on the members of that object. When I copy the setters and getters from the CmsModel base class into the CmsExtraQuestions class, it works fine.
$entity = new CmsExtraQuestions();
$entity->setActive(true);
Why does this not set the member properties of the instantiated object when calling the extended setter? If this is normal - is there a way to add these properties and member functions to the base model so that they can be inherited also?
Rookie mistake, I forgot to add #MappedSuperClass to the inherited model, CmsModel. That allows JPA to map the properties from that class as though they were defined in the model that was inheriting that base class.
#MappedSuperClass
public abstract class CmsModel extends Model {
#Basic(optional = false)
#NotNull
#Column(name = "is_active")
I'm stuck with trying to display data for a One-to-One relationship in Twirl templates (using Play Framework Java - 2.5.10). Basically I have a User model:
package models;
import java.sql.Date;
import javax.persistence.*;
import com.avaje.ebean.Model;
#Entity
#Table(name = "users")
public class User extends Model {
#Id
#Column(name = "id")
public Long id;
#Column(name = "first_name")
public String firstName;
#Column(name = "middle_name")
public String middleName;
#Column(name = "last_name")
public String lastName;
#Column(name = "date_of_birth")
public Date dateOfBirth;
#Column(name = "sex")
public String sex;
#OneToOne
#JoinColumn(name = "time_zone_id")
public TimeZone timeZone;
public static Finder<Long, User> find = new Finder<>(User.class);
}
and the Farmer model:
package models;
import com.avaje.ebean.Model;
import javax.persistence.*;
import java.util.List;
#Entity
#Table(name="farmers")
public class Farmer extends Model {
public enum Status {INACTIVE, ACTIVE}
#Id
#Column(name="id")
public Long id;
#OneToOne(cascade = CascadeType.ALL, fetch=FetchType.EAGER)
#JoinColumn(name="user_id")
public User user;
#Column(name="profile_pic_url")
public String profilePicUrl;
#Column(name="access_url")
public String accessUrl;
#Column(name="status")
public String status = Status.INACTIVE.name();
#OneToMany(mappedBy = "farmer", targetEntity = Farm.class, fetch = FetchType.LAZY)
public List<Farm> farms;
public static Finder<Long, Farmer> find = new Finder<>(Farmer.class);
public static List<Farmer> getAllActive() {
return Farmer.find.where().eq("status", Status.ACTIVE.name()).findList();
}
}
Notice there's a one-to-one with User model with fetch type set to eager. Now, I want to display data of farmers in my template, where a farmer's name is actually the name in the associated User model.
So I did this in my controller:
public class FarmerController extends Controller {
public Result all() {
return ok(farmers.render(Farmer.getAllActive()));
}
public Result farmer(Long id, String url) {
return ok(farmer.render());
}
}
Now this gets me the right farmer data, but when I try to display the name via the User model, I get null. More specifically, writing this results in nulls (I get nullnull, actually):
<div><h4>#(farmer.user.firstName + farmer.user.lastName)</h4></div>
What am I missing?
As discussed at the comments, this is because play-enhancer does not works for views or any Scala code at all. Since Twirl compiles scala.html code to scala code, this compiled code is not touched by the enhancer.
The solution is then to manually create the get for the relationship:
public class Farmer extends Model {
public User getUser() {
return this.user;
}
}
This is Java code and then will be handled as expected. Of course, you have to change your views to use farmer.getUser instead of farm.user.
Also, as stated at the docs, byte code enhancement involves some magic. But you can avoid it at all and just use regular POJOs (with explicitly declared gets and sets).