Spring data rest override nested property POST handler - java

I have a Spring Data Rest repository
public interface ProjectRepository extends CrudRepository<Project, Integer> {}
for the following entity:
#javax.persistence.Entity
#Table(name = "project", uniqueConstraints = {#UniqueConstraint(columnNames = {"owner_id", "title"})})
public class Project {
#Id
#Column(name = "project_id")
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
...
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinTable(name = "project_document", joinColumns = {
#JoinColumn(name = "project_id", nullable = false, updatable = false) },
inverseJoinColumns = { #JoinColumn(name = "document_id",
nullable = false, updatable = false) })
private Set<Document> documents;
...
}
I want to override the POST handler of the nested documents collection and am following the recommended approach.
#RepositoryRestController
public class DocumentController {
#RequestMapping(value = "/projects/{projectId}/documents", method = RequestMethod.POST)
public Document postDocument(
final #PathVariable int projectId,
final #RequestPart("file") MultipartFile documentFile,
final #RequestPart("description") String description
) throws IOException {
...
}
}
But when I fire up the nested POST, it still uses the original Spring generated POST handler and throws unsupported media-type error.
When I change #RepositoryRestController to #RestController, the correct POST handler is used, but the Spring generated CRUD methods for documents subresource of project are not exported.

Try something like this:
#RequiredArgsConstructor
#RepositoryRestController
#RequestMapping("/projects/{id}")
public class ProjectsController {
private final #NonNull DocumentRepository documentRepository;
#PostMapping("/documents")
public ResponseEntity<?> postDocument(#PathVariable("id") Project project, #RequestBody Document document) {
if (project == null) {
throw new Exception("Project is not found!");
}
if (document == null) {
throw new Exception("Document is not found");
}
Document savedDocument = documentRepository.save(document.setProject(project));
return new ResponseEntity<>(new Resource<>(savedDocument), CREATED);
}
}
Working example.

Related

post mapping rest api spring boot cannot get id

Im learning, and so far i created many to many bidirectional database - user can create many groups and group can have many users - and i cannot find a way for my GroupsController Post mapping to work, from my understanding, it requires to get firstly Users id, in order to set the right relationship in Join table for Group, because the relationship should be set only when user create/join group, not when user create sign up procedure. Postman throws 500 and intelliJ:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "com.ilze.highlight.entity.Groups.getId()" is null] with root cause
java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "com.ilze.highlight.entity.Groups.getId()" is null
I use lombok - #Data, #Getter, therefore getId() should be available for use from Group class. My GroupsController with POST mapping when user decides to create a new group:
#RestController
#RequestMapping("api/groups") // pre-path
public class GroupsController{
#Autowired
private GroupsService groupsService;
#Autowired
private UserService userService;
#Autowired
private final GroupsRepository groupsRepository;
#Autowired
private UserRepository userRepository;
public GroupsController(GroupsRepository groupsRepository) {
this.groupsRepository = groupsRepository;
}
#GetMapping("/all-groups")
public List<Groups> getGroups(){
return (List<Groups>) groupsRepository.findAll();
}
#PostMapping("/user/{usersId}/create-group")
public ResponseEntity<Groups> createGroup(#PathVariable(value = "usersId") Long usersId, #RequestBody Groups groupRequest){
Groups group = userRepository.findById(usersId).map(users -> {
long groupsId = groupRequest.getId();
// add and create new group
users.addGroup(groupRequest);
return groupsRepository.save(groupRequest);
}).orElseThrow(() -> new ResourceNotFoundException("Not found user with id = " + usersId));
return new ResponseEntity<>(group, HttpStatus.CREATED);
}
}
Group database class:
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Getter
#Table(name = "group_collection")
public class Groups {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name ="group_name", unique = true, nullable = false, length = 20)
private String groupName;
#Column(name = "size", nullable = false)
private int size;
#Column(name = "strict", nullable = false)
private boolean strict;
#Column(name = "open", nullable = false)
private boolean open;
#Column(name ="description", length = 300)
private String description;
#Column(name = "create_time", nullable = false)
private LocalDateTime createTime;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE,
CascadeType.DETACH,
CascadeType.REFRESH
},
mappedBy = "groups")
#JsonIgnore
private Set<User> users = new HashSet<>();
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
}
And Users class for database:
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "username", unique = true, nullable = false, length = 100)
private String username;
#Column(name = "password", nullable = false)
private String password;
#Column(name = "email", nullable = false)
private String email;
#Column(name = "create_time", nullable = false)
private LocalDateTime createTime;
#Enumerated(EnumType.STRING)
#Column(name = "role", nullable = false)
private Role role;
#Transient
private String accessToken;
#Transient
private String refreshToken;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE,
CascadeType.DETACH,
CascadeType.REFRESH
})
#JoinTable(name = "groups_x_user",
joinColumns = { #JoinColumn(name = "users_id") },
inverseJoinColumns = {#JoinColumn(name = "groups_id")})
private Set<Groups> groups = new HashSet<>();
public void addGroup(Groups group) {
this.groups.add(group);
group.getUsers().add(this);
}
public void removeGroup(long id){
Groups group = this.groups.stream().filter(g ->
g.getId() == id).findFirst().orElse(null);
if(group != null){
this.groups.remove(group);
group.getUsers().remove(this);
}
}
For reference my GroupsService implementation:
#Service
public class GroupsServiceImpl implements GroupsService{
private final GroupsRepository groupsRepository;
public GroupsServiceImpl(GroupsRepository groupsRepository) {
this.groupsRepository = groupsRepository;
}
#Override
public Groups saveGroup(Groups group) {
group.setCreateTime(LocalDateTime.now());
return groupsRepository.save(group);
}
#Override
public Optional<Groups> findByGroupName(String groupName) {
return groupsRepository.findByGroupName(groupName);
}
}
You need to persist the object from request. And since you have Many-2-Many relation, you can insert related object from both sides. In your case: just add existing user to the newly created group
The method will look something like that:
#PostMapping("/user/{usersId}/groups")
public ResponseEntity<Groups> createGroup(#PathVariable(value = "usersId") Long usersId, #RequestBody Groups groupRequest) {
Groups createdGroup = userRepository.findById(usersId)
.map(user -> {
groupRequest.setId(null); // ID for new entry will be generated by entity framework, prevent override from outside
groupRequest.getUsers().add(user); // add relation
return groupsRepository.save(groupRequest);
}).orElseThrow(() -> new ResourceNotFoundException("Not found user with id = " + usersId));
return new ResponseEntity<>(createdGroup, HttpStatus.CREATED);
}

Error: Back reference type (java.util.Set) not compatible with managed type

Goodmorning, i have a problem with a study project. If you consider the ER schema in the image, you can see one relation many-to-many with an associated table. The table Personaggi is composed:
ID_PERSONAGGIO (Auto Generated)
Name
My scope is that of insert with Postman, only one Personaggio without to insert an Albo. Therefore i try to call the API with Postman and pass this Json
{"nome": "Wolverine"}
but i recive this response:
"status": 415,
"error": "Unsupported Media Type",
"message": "Content type 'application/json;charset=UTF-8' not supported"
On the console i have this
2020-10-21 09:06:04.647 WARN 33208 --- [nio-5051-exec-2] .c.j.MappingJackson2HttpMessageConverter : Failed to evaluate Jackson deserialization for type [[simple type, class it.xxxxx.xxxxx.entities.Personaggio]]: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot handle managed/back reference 'defaultReference': back reference type (java.util.Set) not compatible with managed type (it.xxxx.xxxx.entities.Albo)
Controller
#RestController
#RequestMapping("/api/personaggio")
public class PersonaggioController {
#Autowired
PersonaggioService personaggioService;
#RequestMapping(value = "/inserisci", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> insertNewCollezione(#Valid #RequestBody Personaggio personaggio, BindingResult bindingResult) {
HttpHeaders headers = new HttpHeaders();
ObjectMapper mapper = new ObjectMapper();
headers.setContentType(MediaType.APPLICATION_JSON);
ObjectNode responseNode = mapper.createObjectNode();
responseNode.put("code", HttpStatus.OK.toString());
responseNode.put("message", "Inserimento Personaggio " + personaggio.getNome() + " Eseguita Con Successo");
return new ResponseEntity<>(responseNode, headers, HttpStatus.CREATED);
}
}```
#Entity
#Table(name="personaggi")
#Data
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
property = "idPersonaggio",
scope = Integer.class)
public class Personaggio implements Serializable {
public Personaggio(String nome) {
this.nome = nome;
}
public Personaggio() {
// TODO Auto-generated constructor stub
}
private static final long serialVersionUID = -4390691806313380055L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id_personaggio")
private Integer idPersonaggio;
#Column(name = "nome")
private String nome;
#OneToMany(mappedBy="personaggio")
// #JsonManagedReference(value = "listPersonaggiAlbo")
private Set<AssAlboPersonaggi> listPersonaggi = new HashSet<>();
}
/******************** AssAlboPersonaggi *************/
#Entity
#Table(name="ass_albo_personaggi")
#Data
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
scope = AssAlboPersonaggiKey.class)
public class AssAlboPersonaggi {
#EmbeddedId
AssAlboPersonaggiKey id = new AssAlboPersonaggiKey();
#ManyToOne
#EqualsAndHashCode.Exclude
#JoinColumn(name="fk_albo", insertable = false, updatable = false)
// #JsonBackReference(value = "listAlboPersonaggio")
Albo albo;
#ManyToOne
#EqualsAndHashCode.Exclude
#JoinColumn(name="fk_personaggio", insertable = false, updatable = false)
// #JsonBackReference(value = "listPersonaggioAlbo")
Personaggio personaggio;
#Column(name = "flg_protagonista")
private boolean flgProtagonista;
}
/******************** Albo *************/
#Entity
#Table(name="albi")
#Data
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
property = "idAlbo",
scope = Integer.class)
public class Albo implements Serializable{
private static final long serialVersionUID = 6046242309533678207L;
public Albo(double prezzoDiAcquisto, int numeroAlbo, String annoDiPubblicazione, String meseDiPubblicazione, String titoloAlbo) {
this.prezzoDiAcquisto = prezzoDiAcquisto;
this.numeroAlbo = numeroAlbo;
this.annoDiPubblicazione = annoDiPubblicazione;
this.meseDiPubblicazione = meseDiPubblicazione;
this.titoloAlbo = titoloAlbo;
}
public Albo() { }
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="id_albo")
private Integer idAlbo;
#ManyToOne
#EqualsAndHashCode.Exclude
#JoinColumn(name = "collezione", referencedColumnName = "id_collezione")
#JsonManagedReference
private Collezione collezione;
#OneToMany(mappedBy = "albo", cascade = CascadeType.ALL)
// #JsonManagedReference(value = "listAlboPersonaggio")
private Set<AssAlboPersonaggi> listPersonaggi = new HashSet<>();
#Column(name="prezzo_acquisto")
private Double prezzoDiAcquisto;
#Column(name="numero")
private Integer numeroAlbo;
#Column(name="anno_pubb")
private String annoDiPubblicazione;
#Column(name="mese_pubb")
private String meseDiPubblicazione;
#Column(name="titolo_albo")
private String titoloAlbo;

spring mvc 4 + hibernate 5 lazy loading configuration

I have an enterprise project configured by spring mvc4 + hibernate5 that all of its relation are eager and its performance is very bad...So I am transforming all eager relations to lazy step by step...But I see many errors in each step...and it works sometimes properly and sometimes not....
in this example HeaderFromStore is an instnace of RequestHeaders and a child of RequestLine. DeliveryPoint is child of requestHeader and I don't want to fetch deliveryPoint of requestHeader...But if don't use it in select query it couldn't fetch HeaderFromStore !!
I used this query and I get error!
select m from MAMRequestLines m join fetch m.mamRequestHeaders r
left join fetch m.requestHeaderFromStore rr where m.id =:id
If I use this query I don't get error
select m from MAMRequestLines m join fetch m.mamRequestHeaders r
left join fetch m.requestHeaderFromStore rr
join fetch rr.mamDeliveryPoints
left join fetch r.mamDeliveryPoints
join fetch where m.id =:id
RequestLine.java
#Entity(name = "RequestLines")
#Table(name = "_REQUEST_LINES")
//#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property = "#id")
public class RequestLines extends Entity implements Serializable {
#JsonInclude(JsonInclude.Include.NON_EMPTY)
private RequestHeaders requestHeaders;
#JsonInclude(JsonInclude.Include.NON_EMPTY)
private RequestHeaders requestHeaderFromStore;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "_REQUEST_Line_SEQ")
#SequenceGenerator(name = "_REQUEST_Line_SEQ", sequenceName = "_REQUEST_Line_SEQ")
#Column(name = "REQUEST_LINE_ID")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "REQUEST_HEADER_ID", nullable = false)
public RequestHeaders getRequestHeaders() {
return RequestHeaders;
}
public void setRequestHeaders(RequestHeaders requestHeaders) {
this.RequestHeaders = requestHeaders;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "REQUEST_HEADER_FROM_STORE")
public RequestHeaders getRequestHeaderFromStore() {
return requestHeaderFromStore;
}
public void setRequestHeaderFromStore(RequestHeaders requestHeaderFromStore) {
this.requestHeaderFromStore = requestHeaderFromStore;
}
}
RequestHeader.java
#Entity(name = "RequestHeaders")
#Table(name = "REQUEST_HEADERS")
//#JsonInclude(JsonInclude.Include.NON_EMPTY)
public class RequestHeaders extends Entity implements Serializable {
private long id;
// #JsonInclude(JsonInclude.Include.NON_EMPTY)
// #JsonIgnore
private DeliveryPoints DeliveryPoints;
#JsonIgnore
private Set<RequestLines> RequestLinesSet;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "_REQUEST_HEADERS_SEQ")
#SequenceGenerator(name = "_REQUEST_HEADERS_SEQ", sequenceName = "_REQUEST_HEADERS_SEQ")
#Column(name = "REQUEST_HEADER_ID")
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "DELIVERY_POINT_ID", nullable = false)
public DeliveryPoints getDeliveryPoints() {
return DeliveryPoints;
}
public void setDeliveryPoints(DeliveryPoints DeliveryPoints) {
this.DeliveryPoints = DeliveryPoints;
}
#OneToMany(mappedBy = "RequestHeaders", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
#OnDelete(action = OnDeleteAction.CASCADE)
public Set<RequestLines> getRequestLinesSet() {
return RequestLinesSet;
}
public void setRequestLinesSet(Set<RequestLines> RequestLinesSet) {
this.RequestLinesSet = RequestLinesSet;
}
}
exception:
No serializer found for class
org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no
properties discovered to create BeanSerializer (to avoid exception,
disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference
chain:
domain.RequestLine["HeaderFromStore"]->.domain.RequestHeaders["DeliveryPoint"]->domain.DeliveryPoint_$$_jvst393_f["handler"])
notice that I used JsonIgnore and JsonInclude(on fields and class) but none of them doesn't work...
Edit:
I finally found this solution to avoid exception and ignoring unwanted properties.
I added this part of code to WebMvcConfig extends WebMvcConfigurerAdapter class:
{
public MappingJackson2HttpMessageConverter jacksonMessageConverter(){
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
//Registering Hibernate4Module to support lazy objects
mapper.registerModule(new Hibernate4Module());
messageConverter.setObjectMapper(mapper);
return messageConverter;
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//Here we add our custom-configured HttpMessageConverter
converters.add(jacksonMessageConverter());
super.configureMessageConverters(converters);
}
But I have another problem now...all post requests will receive with null properties in request body ....for example in this code all properties of "requestHeaders" in input is null or empty!
#RequestMapping(value = "/requestHeader/", method = RequestMethod.POST, consumes = {"application/json"})
public ResponseEntity<Void> createRequestHeaders(#RequestBody RequestHeaders requestHeaders, UriComponentsBuilder ucBuilder) {
requestHeaders.setDeliveryPoints(deliveryPointsService.find(requestHeaders.getDeliveryPointsId()));
requestHeadersService.add(requestHeaders);
HttpHeaders headers = new HttpHeaders();
return new ResponseEntity<Void>(headers, HttpStatus.CREATED);
}
you should add each entities which have relation this annotation at the top of class definition.
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
details are explained here and here
I hope these solves your problem.

Spring hibernate not persisting object nodes

I've the following Entity :
#Entity
public class Pages {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#OneToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
#JoinColumn(name="page")
private List<Pages_lang> pages_lang;
private long updated;
private Boolean deleted = false;
private long position;
private String icon;
#OneToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY, mappedBy = "parent")
private List<Pages> children;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Pages parent;
And this one :
#Entity
public class Pages_lang {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "language")
private Language language;
private String title;
#Column(name = "text", columnDefinition = "TEXT")
private String text;
#Column(name = "plaintext", columnDefinition = "TEXT")
private String plaintext;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)//, mappedBy = "articles_lang")
#JoinColumn(name = "pages_lang")
private List<Images> images;
When I persist (create) in the DB whit this method, it works without any problem :
#RequestMapping(method = RequestMethod.POST)
public ResponseEntity<String> postPageTopLevel(#RequestBody Pages posted) {
Pages inserted = new Pages();
inserted.merge(posted);
Pages result = dao.save(inserted);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setLocation(ServletUriComponentsBuilder
.fromCurrentRequest().path("/" + result.getId())
.buildAndExpand(result.getId()).toUri());
return new ResponseEntity<String>(getGson().toJson(result), httpHeaders, HttpStatus.CREATED);
}
But when I want to update an attribute of the Pages_lang object and I persist the Page object, the attribute isn't modified on the DB.
Here, the method to update :
#RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public ResponseEntity<String> putPage(#PathVariable Long id, #RequestBody Pages puted) {
System.out.println("");System.out.println("");System.out.println("");System.out.println("");System.out.println("");System.out.println("");
if (id != puted.getId())
return new ResponseEntity<String>("URI unmatch element", new HttpHeaders(), HttpStatus.FORBIDDEN);
Pages page = dao.findOne(id);
if (page == null)
return new ResponseEntity<String>(null, new HttpHeaders(), HttpStatus.NOT_FOUND);
dao.save(puted);
return new ResponseEntity<String>(getGson().toJson(puted), new HttpHeaders(), HttpStatus.OK);
}
I think the problem is related with the Cascade type of the Pages_lang attribute in the Pages Object.
Anyone can help me ?
Regards
Edit :
The dao.save() method is the basic method included in the Spring Repository.
BaseRepository.java
#NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends Repository<T, ID> {
void delete(T deleted);
Iterable<T> findAll();
T findOne(ID id);
T save(T persisted);
Iterable<T> save(Iterable<T> persited);
}

Mock loading of navigation properties

This is a very simplified version of my code, that illustrates the specific problem.
Is there any way that I can control what happens when accountProductRepository.refresh() is called from the test?
Somehow I need to set the ProductPojo on the AccountProductPojo created in the buyProduct() method, so I don't get a null pointer when accessing the getProduct().getName() property.
refresh uses javax.persistence.EntityManager.refresh() to load the navigation properties based on the id's set in the buyProduct() method.
public class ProductServiceTest {
#InjectMocks
IProductService productService = new ProductService();
#Mock
IWriteANoteService writeANoteService;
#Mock
IAccountProductRepository accountProductRepository;
#Test
public void buyProductTest() {
productService.buyProduct(1l, 1l);
}
}
#Service
public class ProductService implements IProductService {
#Autowired
IWriteANoteService writeANoteService;
#Autowired
IAccountProductRepository accountProductRepository:
public void buyProduct(Long productId, Long accountId) {
AccountProductPojo accountProduct = new AccountProductPojo();
accountProduct.setProductId(productId);
accountProduct.setAccountId(accountId);
accountProductRepository.persist(accountProduct);
// load navigation properties
accountProductRepository.refresh(accountProduct);
writeANoteService.writeAccountNote(accountId, "Bought product " + accountProduct.getProduct().getName());
}
}
#Entity
#Table(name = "account_product")
public class AccountProductPojo {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "account_id")
private Long accountId;
#Column(name = "product_id")
private Integer productId;
#ManyToOne
#JoinColumn(name = "product_id", insertable = false, updatable = false)
private ProductPojo product;
#OneToOne(fetch = FetchType.LAZY, targetEntity = AccountPojo.class)
#JoinColumn(name = "account_id", insertable = false, updatable = false)
private AccountPojo account;
// getters and setters
}
This seems to be a fairly classic case of mocking a void method.
You could try something like this:
Mockito.doAnswer(new Answer() {
public Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
AccountProductPojo accountProduct = (AccountProductPojo) args[0];
accountProduct.setProduct(new ProductPojo(PRODUCT_ID_CONSTANT, PRODUCT_NAME_CONSTANT));
return null;
}}).when(accountProductRepository).refresh(Mockito.any());
The key here is that when refresh() is called on the mock you call setProduct() on the POJO which was passed as an argument to the refresh() call in order to avoid the later null pointer exception.

Categories