I have a spring rest api application that is using HATEOAS/PagingAndSortingRepository to do most of the heavy lifting.
I have implemented caching using guava but I am having issues where when the user cancels the request midway through an api call, it caches the incomplete json and re-serves it for 60 seconds.
I am trying to use the unless="" parameter of the #Cacheable annotation. Previously, I just used unless="#result == null" but that does not handle incomplete or invalid json.
This does not seem to work either. So now I am trying to use com.google.gson.JsonParser to parse the result and invalidate if applicable.
Repository
#RepositoryRestResource(path = "products", collectionResourceRel = "products")
public interface ProductEntityRepository extends PagingAndSortingRepository<ProductEntity, String> {
JsonParser parser = new JsonParser();
#Cacheable(value = CacheConfig.STORE_CACHE)
ProductEntity findByName(String name);
}
Cache Config
public final static String PRODUCTS_CACHE = "products";
#Bean
public Cache productsCache() {
return new GuavaCache(PRODUCTS_CACHE, CacheBuilder.newBuilder()
.expireAfterWrite(60, TimeUnit.SECONDS)
.build());
}
How do I detect invalid json in the unless="" parameter?
I figured out my own issue!
When I interrupted the api request to localhost/products and re-requested, I finally saw an error about not being able to fetch a onetomany mapping. I believe the error was lazy initialization error for a collection.
I solved this issue by adding #LazyCollection(LazyCollectionOption.FALSE) to my models where the #OneToMany and #ManyToOne mappings were decalared.
For example:
#Entity(name = "product")
#Table(name = "products", schema = "${DB_NAME}", catalog = "")
public class ProductEntity {
private Integer id;
private String name;
private List shipments = new ArrayList<>();
#Id
#Column(name = "id", nullable = false)
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#Basic
#Column(name = "name", nullable = false, length = 10)
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
#OneToMany(mappedBy = "shipmentID", targetEntity=ShipmentEntity.class)
#LazyCollection(LazyCollectionOption.FALSE)
public Collection<ShipmentEntity> getShipments() { return shipments; }
public void setShipments(Collection<ShipmentEntity> shipments) { this.shipments = shipments; }
}
Related
I'm doing a restful app in Spring boot,jpa,mysql. I have annoted some of my model fields #NotBlank to print an error in the creation of an object if those fields are blank.
Now when i'm updating, I don't want to get that error if I don't set some fields in my json body.My goal is to update just the fields which are present.
So I want to know if there is a way not to consider an #NotBlank in my updating method.
This is the code source :
For the Entity
public class Note implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotBlank(name)
private String title;
#NotBlank
private String content;
//Getters and Setters
}
The controller
#RestController
#RequestMapping("/api")
public class NoteController {
#Autowired
NoteRepository noteRepository;
// Create a new Note
#PostMapping("/notes")
public Note createNote(#Valid #RequestBody Note note) {
return noteRepository.save(note);
}
// Update a Note
#PutMapping("/notes/{id}")
public Note partialUpdateNote(#PathVariable(value = "id") Long noteId,
#RequestBody Note noteDetails) {
Note note = noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
//copyNonNullProperties(noteDetails, note);
if(note.getTitle()!= null) {
note.setTitle(noteDetails.getTitle());
}else {
note.setTitle(note.getTitle());
}
if(note.getContent()!= null) {
note.setContent(noteDetails.getContent());
}else {
note.setContent(note.getContent());
}
Note updatedNote = noteRepository.save(note);
return updatedNote;
}
// Delete a Note
#DeleteMapping("/notes/{id}")
public ResponseEntity<?> deleteNote(#PathVariable(value = "id") Long noteId) {
Note note = noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
noteRepository.delete(note);
return ResponseEntity.ok().build();
}
}
ResourceNotFoundException is the class responsible to throws errors.
You can use groups for that.
Add two interfaces CreateGroup and UpdateGroup.
Use them by this way:
#NotBlank(groups = CreateGroup.class)
#Null(groups = UpdateGroup.class)
private String title;
In the create endpoint
#Valid #ConvertGroup(from = Default.class, to = CreateGroup.class) Note note
In the update endpoint
#Valid #ConvertGroup(from = Default.class, to = UpdateGroup.class) Note note
Probably you don't need UpdateGroup. It is just to show a common approach.
Also for the nested objects inside Note something like
#ConvertGroup(from = CreateGroup.class, to = UpdateGroup.class)
can be used.
I am using Spring Boot REST Web Services and Angular 5 as a frontend, well I have a model class for hibernating like this :
#Entity
public class Title {
private Integer id;
private String name;
private Date releaseDate;
private Time runtime;
private String storyline;
private String picture;
private String rated;
private String type;
private Double rating;
private Integer numberOfVotes;
private Timestamp inserted;
private Set<Genre> genres = new HashSet<>();
private List<TitleCelebrity> titleCelebrities;
private List<TitleMedia> titleMedia;
// Basic getters and setter
#ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
#JoinTable(name = "title_genre", joinColumns = { #JoinColumn(name = "title_id") }, inverseJoinColumns = { #JoinColumn(name = "genre_id") })
public Set<Genre> getGenres() {
return genres;
}
public void setGenres(Set<Genre> genres) {
this.genres = genres;
}
#OneToMany(mappedBy = "title", cascade = CascadeType.ALL)
public List<TitleCelebrity> getTitleCelebrities() {
return titleCelebrities;
}
public void setTitleCelebrities(List<TitleCelebrity> titleCelebrities) {
this.titleCelebrities = titleCelebrities;
}
#OneToMany(mappedBy = "title", cascade = CascadeType.ALL)
public List<TitleMedia> getTitleMedia() {
return titleMedia;
}
public void setTitleMedia(List<TitleMedia> titleMedia) {
this.titleMedia = titleMedia;
}
}
And here's my REST controller
#RestController
#RequestMapping("titles")
#CrossOrigin(origins = {"http://localhost:4200"})
public class TitleController {
private TitleService titleService;
#Autowired
public void setTitleService(TitleService titleService) {
this.titleService = titleService;
}
// Api to get all the movies ordered by release date
#GetMapping("movies")
public List<Title> getAllMoviesOrderByReleaseDateDesc() {
return this.titleService.findByTypeOrderByReleaseDateDesc("movie");
}
#GetMapping("movies/{id}")
public Title findById(#PathVariable Integer id) {
return this.titleService.findById(id);
}
}
What I want is when I make a request to the first method '/movies' i don't want the collection of Telemedia, but if I make a request to the second method '/movies/id' i want the collection of Telemedia.
of course, the annotation #JsonIgnore will ignore the collection whatever the request is.
It may be better to create two models in this case; one to represent the first response and another to represent the second response.
You could also set the collection to null in your second request before sending it back.
You cannot accomplish this with #JsonIgnore alone as you cannot perform conditional logic in annotations.
I am starting working with Spring Boot. My aim is to make a limited search retrieving data from a database. I want to add multiple parameters in the query of the url.
So far I was able using the seek: http://localhost:8080/wsr/search/, to get a full search of the data in the database. But what I want is delimit the search under several conditions adding parameters in the url in the browser as for instance:
http://localhost:8080/data/search/person?name=Will&address=Highstreet&country=UK
http://localhost:8080/data/search/person?name=Will&name=Angie
http://localhost:8080/data/search/person?name=Will&name=Angie&country=UK
The problem I found is that I can't find the way to work with more than one condition. The only thing I got to make it work, is:
http://localhost:8080/data/search/person?name=Will
I surfed the web but no results for this exact problem, too much information but impossible to find this.
The code I have is:
#Entity
#Table(name = "person")
public class Person {
#Id
#GeneratedValue
#Column(name = "id")
private int id;
#Column(name = "name")
private String name;
#Column(name = "address")
private String address;
#Column(name = "country")
private String country;
public Value() {
}
public Value(int id, String name, String address, String country) {
this.id = id;
this.name = name;
this.address = address;
this.country = country;
}
//all getters and setters
}
public class Implementation {
#Autowired
private DataBase dataBase;
public List<Value> findById(#PathVariable final int id) {
return dataBase.findById(id);
}
public List<Value> findByName(#PathVariable final String name) {
return dataBase.findByName(name);
}
public List<Value> findByAddress(#PathVariable final String address) {
return dataBase.findByAddress(address);
}
public List<Value> findByCountry(#PathVariable final String country) {
return dataBase.findByCountry(country);
}
}
//#Component
#RepositoryRestResource(collectionResourceRel = "person", path = "data")
public interface DataBase extends JpaRepository<Value, Integer>{
public List<Value> findAll();
#RestResource(path = "ids", rel = "findById")
public List<Value> findById(#Param("id") int id) throws ServiceException;
#RestResource(path = "name", rel = "findByName")
public List<Value> findByName(#Param("name") String name) throws ServiceException;
#RestResource(path = "address", rel = "findByAddress")
public List<Value> findByAddress(#Param("address") String address) throws ServiceException;
#RestResource(path = "country", rel = "findByCountry")
public List<Value> findByCountry(#Param("country") String country) throws ServiceException;
}
Hope you can help me putting me in the correct way of what should do or is wrong. If possible some code will also be highly appreciated.
Best regards
You can use #RequestParam("nameParameter")annotation to map all the parameters you want. Let's say you have url like :
http://localhost:8080/data/search/person?name=Will&country=UK
then you can have an api like:
...
#RequestMapping(value = "/person")
public String api(#RequestParam("name") String name, #RequestParam("country") String country)
...
I've worked with Spring and Hibernate. Now having a look at Spring Data JPA (2.0.3) with JPA 2.2
AgencyTicketType
#Entity
#Table(name = "agency_ticket_type", catalog = "test")
public class AgencyTicketType implements java.io.Serializable {
private Long id;
private String name;
private Agency agency;
private Set<AgencyTicketCategory> agencyTicketCategories = new HashSet<AgencyTicketCategory>(0);
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id", unique = true, nullable = false)
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
#Column(name = "name", nullable = false, length = 100)
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "agency_id", nullable = false)
public Agency getAgency() {
return this.agency;
}
public void setAgency(Agency agency) {
this.agency = agency;
}
#OneToMany(fetch = FetchType.LAZY, mappedBy = "agencyTicketType")
public Set<AgencyTicketCategory> getAgencyTicketCategories() {
return this.agencyTicketCategories;
}
public void setAgencyTicketCategories(Set<AgencyTicketCategory> agencyTicketCategories) {
this.agencyTicketCategories = agencyTicketCategories;
}
}
AgencyTicketCategory
#Entity
#Table(name = "agency_ticket_category", catalog = "waytest")
public class AgencyTicketCategory implements java.io.Serializable {
private Long id;
private AgencyTicketType agencyTicketType;
private String name;
private BigDecimal price;
private Set<TripTicket> tripTickets = new HashSet<TripTicket>(0);
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id", unique = true, nullable = false)
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "agency_ticket_type_id", nullable = false)
public AgencyTicketType getAgencyTicketType() {
return this.agencyTicketType;
}
public void setAgencyTicketType(AgencyTicketType agencyTicketType) {
this.agencyTicketType = agencyTicketType;
}
#Column(name = "name", nullable = false, length = 100)
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
#Column(name = "price", nullable = false, precision = 8)
public BigDecimal getPrice() {
return this.price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
#OneToMany(fetch = FetchType.LAZY, mappedBy = "agencyTicketCategory")
public Set<TripTicket> getTripTickets() {
return this.tripTickets;
}
public void setTripTickets(Set<TripTicket> tripTickets) {
this.tripTickets = tripTickets;
}
}
Repository
public interface TicketTypeRepository extends JpaRepository<AgencyTicketType, Long> {
#EntityGraph(attributePaths={ "agencyTicketCategories" }, type=EntityGraphType.LOAD)
#Query("select type from AgencyTicketType type where type.agency.code=?1")
List<AgencyTicketType> findByAgency(String agencyCode);
}
Service
#Service
public class TicketServiceImpl implements TicketService {
#Autowired private TicketTypeRepository ticketType;
#Transactional(readOnly=true)
#Override
public List<AgencyTicketType> findByName(String code) {
return ticketType.findByAgency(code);
}
}
When debugged on Service, it seems, the query eagerly fetches all the lazy loaded properties - agency, agencyTicketCategories - and all their inner lazy loaded properties, which leads to JSON serilization error.
Need to fetch only these
AgencyTicketTypes [
{
id, name,
agencyTicketCategories [
{id,name,price},....
]
},.....
]
Can I do this with #EntityGraph? What I am missing?
Specifying lazy loading is only a hint for the JPA provider. Depending on the provider you use (Hibernate, EclipseLink etc.) it may be completely ignored and the dependencies may be eagerly fetched.
What you need to do is configure how your classes are mapped to json. Assuming you are using Jackson you may need to use annotations like #JsonIgnore or #JsonView. You may also map your class that only has the fields you need.
You can use Jackson annotations #JsonBackReference/#JsonManagedReference. They address problem of infinite recursion with bidirectional links in object model. As far as I understand it is your case.
See http://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion for more information.
One thing to point is that debugging while the transaction is open (touching the collection) will cause it to be loaded even if at real time it doesn't .. the other thing is that as #Apokralipsa mentioned , LAZY loading is just a hint that can be totally ignored and should never be relied upon whatever technique you are using
in my current implementation using Spring-Boot, -HATEOAS, -Rest-Data I'm trying to spare some further rest calls and enhance my rest resource for credits to also deliver relations of a credit (see below account as ManyToOne and creditBookingClassPayments as OneToMany).
The problem now is that I'm not able to get it run. The call always delivers empty relations. I really would appreciate some help on this.
Here are the surroundings:
Credit.java
#Entity
#Getter
#Setter
public class Credit {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Setter(NONE)
#Column(name = "id")
private Long itemId;
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name="account_id", nullable = false)
private Account account;
#OneToMany(mappedBy = "credit")
private List<CreditBookingClassPayment> creditBookingClassPayments = new ArrayList<>();
#NotNull(message="Please enter a valid short name.")
#Column(length = 10, nullable = false)
private String shortName;
#NotNull(message="Please enter a valid name.")
#Column(nullable = false)
private String name;
...
}
CreditRepositoryCustomImpl.java
uses QueryDsl to enhance the credit resource with its realation
...
#Override
public List<Credit> findDistinctByAccountItemIdNew(Long accountId) {
QCredit credit = QCredit.credit;
QAccount account = QAccount.account;
QCreditBookingClassPayment creditBookingClassPayment = QCreditBookingClassPayment.creditBookingClassPayment;
QBookingClass bookingClass = QBookingClass.bookingClass;
BooleanExpression hasAccountItemId = credit.account.itemId.eq(accountId);
List<Credit> credits = from(credit).where(hasAccountItemId)
.innerJoin(credit.account, account)
.leftJoin(credit.creditBookingClassPayments, creditBookingClassPayment)
.leftJoin(creditBookingClassPayment.bookingClass, bookingClass).groupBy(credit.itemId).fetch();
return credits;
}
...
CreditController.java
looking into responseBody here all (account and credit payments) is available for credits
#RepositoryRestController
public class CreditController {
#Autowired
private CreditRepository creditRepository;
#RequestMapping(value = "/credit/search/findAllByAccountItemIdNew", method= RequestMethod.GET, produces = MediaTypes.HAL_JSON_VALUE)
#ResponseBody
public ResponseEntity<Resources<PersistentEntityResource>> findAllByAccountItemIdNew(#RequestParam Long accountId, PersistentEntityResourceAssembler persistentEntityResourceAssembler) {
List<Credit> credits = creditRepository.findDistinctByAccountItemIdNew(accountId);
Resources<PersistentEntityResource> responseBody = new Resources<PersistentEntityResource>(credits.stream()
.map(persistentEntityResourceAssembler::toResource)
.collect(Collectors.toList()));
return ResponseEntity.ok(responseBody);
}
}
CreditResourceIntegrTest.java
here creditResourcesEntity hold the credit but account is null and creditBookingClassPayment is an empty array
#Test
public void testFindAllByAccountItemId() throws URISyntaxException {
URIBuilder builder = new URIBuilder(creditFindAllByAccountItemIdRestUrl);
builder.addParameter("accountId", String.valueOf(EXPECTED_ACCOUNT_ID));
builder.addParameter("projection", "base");
RequestEntity<Void> request = RequestEntity.get(builder.build())
.accept(MediaTypes.HAL_JSON).acceptCharset(Charset.forName("UTF-8")).build();
ResponseEntity<Resources<Resource<Credit>>> creditResourcesEntity =
restTemplate.exchange(request, new ParameterizedTypeReference<Resources<Resource<Credit>>>() {});
assertEquals(HttpStatus.OK, creditResourcesEntity.getStatusCode());
//assertEquals(EXPECTED_CREDIT_COUNT, creditResourcesEntity.getBody().getContent().size());
}
Do I miss something?
Thanks for your help!
Karsten
Okay, PersistentEntityResourceAssembler doesn't support relations. But this could be handled by using projections.
CreditProjection.java
#Projection(name = "base" , types = Credit.class)
public interface CreditProjection {
String getShortName();
String getName();
List<CreditBookingClassPaymentProjection> getCreditBookingClassPayments();
BigDecimal getValue();
BigDecimal getInterestRate();
BigDecimal getMonthlyRate();
}
CreditBookingClassPaymentProjection.java
#Projection(name = "base" , types = CreditBookingClassPayment.class)
public interface CreditBookingClassPaymentProjection {
BookingClass getBookingClass();
CreditPaymentType getCreditPaymentType();
}
CreditController.java
#RepositoryRestController
public class CreditController {
#Autowired
private ProjectionFactory projectionFactory;
#Autowired
private CreditRepository creditRepository;
#RequestMapping(value = "/credit/search/findAllByAccountItemIdNew", method = RequestMethod.GET, produces = MediaTypes.HAL_JSON_VALUE)
#ResponseBody
public ResponseEntity<Resources<?>> findAllByAccountItemIdNew(#RequestParam Long accountId,
PersistentEntityResourceAssembler persistentEntityResourceAssembler) {
List<Credit> credits = creditRepository.findDistinctByAccountItemIdNew(accountId);
List<PersistentEntityResource> creditResources = new ArrayList<>();
for (Credit credit : credits) {
// credit.getCreditBookingClassPayments()
PersistentEntityResource creditResource = persistentEntityResourceAssembler.toResource(credit);
creditResources.add(creditResource);
}
Resources<CreditProjection> responseBody = new Resources<CreditProjection>(credits.stream()
.map(credit -> projectionFactory.createProjection(CreditProjection.class, credit))
.collect(Collectors.toList()));
return ResponseEntity.ok(responseBody);
}
}