Using Spring Data Projection on DBRef objects - spring-boot

I'm starting to learn Spring Data on a pretty simple project using a MongoDB database, and I'm having some trouble when using DBRef - and maybe in how to model a NoSQL db in general
Description
My project should organize a simple competition with a organizer (CD) and one-to-many participants. Because people can participate in multiple competitions I made respositories for both Competition and Person.
The complete code can be seen on GitHub : https://github.com/elkjaerit/rest-sample
Here is the base classes:
public class Competition {
#Id private String id;
private String name;
#DBRef
private Person organizer;
private List<Participant> participants = new ArrayList<>();
}
public class Participant {
private String freq;
#DBRef
private Person person;
}
public class Person {
#Id
private String id;
private String name;
}
Repositories:
#RepositoryRestResource(collectionResourceRel = "competition", path = "competition")
public interface CompetitionRepository extends MongoRepository<Competition, String> {
}
#RepositoryRestResource(collectionResourceRel = "person", path = "person")
public interface PersonRepository extends MongoRepository<Person, String> {
}
Problem
When I'm requesting a competition resource I'm not getting enough info on the participants - only "freq" field is shown. I've tried using #Projection and managed to get it to work for the organizer but I don't know how to get the person object for the participant ?
Result without projection
{
"_links": {
"competition": {
"href": "http://localhost:8080/competition/5710b32b03641c32671f885a{?projection}",
"templated": true
},
"organizer": {
"href": "http://localhost:8080/competition/5710b32b03641c32671f885a/organizer"
},
"self": {
"href": "http://localhost:8080/competition/5710b32b03641c32671f885a"
}
},
"name": "Competition #1",
"participants": [
{
"freq": "F0"
},
{
"freq": "F1"
},
{
"freq": "F2"
},
{
"freq": "F3"
}
]
}
And with projection
{
"_links": {
"competition": {
"href": "http://localhost:8080/competition/5710b32b03641c32671f885a{?projection}",
"templated": true
},
"organizer": {
"href": "http://localhost:8080/competition/5710b32b03641c32671f885a/organizer"
},
"self": {
"href": "http://localhost:8080/competition/5710b32b03641c32671f885a"
}
},
"name": "Competition #1",
"organizer": {
"name": "Competition organizer"
},
"participants": [
{
"freq": "F0"
},
{
"freq": "F1"
},
{
"freq": "F2"
},
{
"freq": "F3"
}
]
}
Any suggestions ?

You may be able to use SPEL to call the getter for your related documents.
Your projection may look something like this -
#Projection(name = "comp", types = {Competition.class})
public interface CompetitionProjection {
String getName();
Person getOrganizer();
#Value("#{target.getParticipants()}")
List<Participant> getParticipants();
}

Related

springboot mongodb crud update only changed fields

Hello i have springboot with mongodb (spring-boot-starter-data-mongodb)
My problem is if I send only one or only the fields I want to change so the other values are set to null. I found something on the internet like #DynamicUpdate but not working on mongodb can you help me with this problem. I'm a beginner, I don't know how to help and it's quite important for me, if you need more code or more information, I'll write in the comment. I hope I have described the problem sufficiently. :)
MY POJO:
#Data
#Getter
#Setter
#NoArgsConstructor
public class Person {
#Id
private String id;
private String firstName;
private String lastName;
private boolean enabled;
private String note;
Repo
#Repository
public interface PersonRepository extends MongoRepository <Person, String> {
}
i have this call
#PutMapping("/{id}")
#ResponseBody
public void UpdatePerson (#PathVariable String id , #RequestBody Person person) {
personRepository.save(person);
}
#GetMapping(path = "/{id}")
public Person getPersonByid(#PathVariable String id ){
return personRepository.findById(id).orElseThrow(PersonNotFound::new);
}
sample:
get call before update :
{
"id": "5fc940dc6d368377561dbb02",
"firstName": "Rambo",
"lastName": "Norris",
"enabled": true,
"note": "hello this is my first note from you",
}
put call :
{
"id": "5fc940dc6d368377561dbb02",
"firstName": "Chuck"
}
get call after update :
{
"id": "5fc940dc6d368377561dbb02",
"firstName": "Chuck",
"lastName": null,
"enabled": false,
"note": null,
}
what I would like
get call before update :
{
"id": "5fc940dc6d368377561dbb02",
"firstName": "Rambo",
"lastName": "Norris",
"enabled": true,
"note": "hello this is my first note from you",
}
put call :
{
"id": "5fc940dc6d368377561dbb02",
"firstName": "Chuck"
}
get call after update :
{
"id": "5fc940dc6d368377561dbb02",
"firstName": "Chuck",
"lastName": "Norris",
"enabled": true,
"note": "hello this is my first note from you",
}
You are inserting a new collection instead of updating. First, you need to get the old value from mongodb, then you need to update the collection, then save to DB.
Use the below code in #putmapping.
#PutMapping("/{id}")
#ResponseBody
public void UpdatePerson (#PathVariable String id , #RequestBody Person person) {
Person personFromDB = personRepository.findById(person.getId());
personFromDB.setFirstName(person.getFirstName());
personRepository.save(personFromDB);
}
Try updating like this
#PutMapping("/{id}")
public ResponseEntity<Person> UpdatePerson (#PathVariable String id , #RequestBody
Person person) {
Optional<Person> personData = personRepository.findById(id);
if (personData.isPresent()) {
Person _tutorial = personData.get();
if(!StringUtils.isEmpty(person.getFirstName())) {
_tutorial.setFirstName(person.getFirstName());
}
if(!StringUtils.isEmpty(person.getLastName())) {
_tutorial.setLastName(person.getLastName());
}
if(!StringUtils.isEmpty(person.getNote())) {
_tutorial.setNote(person.getNote());
}
if(!StringUtils.isEmpty(tutorial.isEnabled())) {
_tutorial.setEnabled(tutorial.isEnabled());
}
return new ResponseEntity<>(repo.save(_tutorial), HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}

How can I get the nested list of object with reactive couchbase?

I recently started reviewing/learning some about reactive couchbase and I'm trying to implement a sample using that technology with spring data and spring boot, I have the following models:
#Document
public class Person {
#Id
private String id;
#Field
private String name;
#Field
private String lastName;
#Field
private List<Address> address;
// Getters & Setters
}
#Document
public class Address {
#Id
private String code;
#Field
private String mainStreet;
#Field
private String secondStreet;
// Getters & Setters
}
Repositories:
#N1qlPrimaryIndexed
#ViewIndexed(designDoc = "address")
public interface AddressRepository extends ReactiveCouchbaseRepository<Address, String> {
}
#N1qlPrimaryIndexed
#ViewIndexed(designDoc = "person")
public interface PersonRepository extends ReactiveCouchbaseRepository<Person, String> {
}
Service:
public interface PersonService {
Flux<Person> getAllPersons();
Mono<Person> getPerson(String id);
Flux<Address> getAddressesByPerson(String id);
}
#Service
public class PersonServiceImpl implements PersonService {
#Autowired
private PersonRepository personRepository;
#Override
public Flux<Person> getAllPersons() {
return personRepository.findAll();
}
#Override
public Mono<Person> getPerson(String id) {
return personRepository.findById(id);
}
#Override
public Flux<Address> getAddressesByPerson(String id) {
return null;
}
}
Controller:
#RestController
#RequestMapping("/sample_couchbase")
public class PersonController {
#Autowired
private PersonService personService;
#GetMapping("/people")
public Flux<Person> getAllPersons() {
return personService.getAllPersons();
}
#GetMapping("/person/{id}")
public Mono<ResponseEntity<Person>> getPersonsById(#PathVariable String id) {
return personService.getPerson(id)
.map(person -> ResponseEntity.status(HttpStatus.OK).body(person))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
}
As so far this works well, I can retrieve all the people and also filter by an id of a specific person, on the other hand I would like to retrieve all list of addresses of a specific person, I mean I have this document in couchbase:
{
"id": "1",
"name": "Scooby",
"lastName": "Doo",
"address": [
{
"code": "A1",
"mainStreet": "AAA",
"secondStreet": "BBB",
"phone": "11111",
"place": "home"
},
{
"code": "A2",
"mainStreet": "CCC",
"secondStreet": "DDD",
"phone": "22222",
"place": "work"
},
{
"code": "A3",
"mainStreet": "EEE",
"secondStreet": "FFF",
"phone": "33333",
"place": "treasury"
}
],
"classType": "com.jcalvopinam.model.Person"
}
When I call the service e.g.: http://localhost:8080/sample_couchbase/person/1/addresses I want to get this:
"address": [
{
"code": "A1",
"mainStreet": "AAA",
"secondStreet": "BBB",
"phone": "11111",
"place": "home"
},
{
"code": "A2",
"mainStreet": "CCC",
"secondStreet": "DDD",
"phone": "22222",
"place": "work"
},
{
"code": "A3",
"mainStreet": "EEE",
"secondStreet": "FFF",
"phone": "33333",
"place": "treasury"
}
]
I imagined create in the repository a method something like this:
Mono<Person> findById(String id);
And in the service layer I imagine getting the whole object and then filtering by Address, but this is something different from what I'm used to doing, now I'm using Mono and Flux and not a simple object or a List, so I don't know how to do that, can someone give me an idea? or is there a better solution?
I found an easy way to retrieve the Addresses of a specific Person, I used a query where I specified what I want to retrieve:
#Query("SELECT addresses, META(sample).id as _ID, META(sample).cas as _CAS FROM `sample` WHERE id = $1")
Flux<Person> findAddressesByPerson(String id);
I noticed that it is necessary to specify the META(sample).id as _ID and META(sample).cas as _CAS attributes to avoid the following error:
org.springframework.data.couchbase.core.CouchbaseQueryExecutionException: Unable to retrieve enough metadata for N1QL to entity mapping, have you selected _ID and _CAS?; nested exception is rx.exceptions.OnErrorThrowable$OnNextValue: OnError while emitting onNext value: com.couchbase.client.java.query.DefaultAsyncN1qlQueryRow.class
If you want to take a look at the implementation in more detail, here is the sample: https://github.com/juanca87/sample-couchbase-rx

Entry creation returns a 201 but accessing it returns 404

I have a simple Spring Data Rest implementation of user creation using Hibernate and MongoDB.
User.java:
#Data
#Entity
#RequiredArgsConstructor
#JsonIgnoreProperties(ignoreUnknown = true)
public class User {
private #Id String username;
private String about;
}
UserRepository.java
#PreAuthorize("hasRole('ROLE_USER')")
#CrossOrigin(methods = {GET, PUT, POST})
public interface UserRepository extends MongoRepository<User, String> {
#Override
#PreAuthorize("hasRole('ROLE_ADMIN')")
<S extends User> S save(S s);
}
Then I make a POST call to /users with this body:
{
"username": "username1",
"about": "example"
}
I get a 201 Created response with the following body:
{
"about": "example",
"_links": {
"self": {
"href": "http://localhost:8080/api/users/username1"
},
"user": {
"href": "http://localhost:8080/api/users/username1"
}
}
}
I make a GET request to /users to see if the user was indeed added and this response is returned rightfully so:
{
"_embedded": {
"users": [
{
"about": "example",
"_links": {
"self": {
"href": "http://localhost:8080/api/users/username1"
},
"user": {
"href": "http://localhost:8080/api/users/username1"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/users{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/api/profile/users"
}
},
"page": {
"size": 20,
"totalElements": 1,
"totalPages": 1,
"number": 0
}
}
THE PROBLEM
But then I access the URL of the user provided in the links, i.e., http://localhost:8080/api/users/username1 but I get a 404 Not Found response.
What am I doing wrong? I've tried looking through examples and documentation but nothing seems to do the work. If I add the #AutoGenerated annotation it works, but I obviously want the id to be provided by the request as the username.
In User.java, I changed the declaration of username to this:
#Id
#JsonProperty("username")
#NotBlank
private String id;
MongoDb requires _id as the primary key for every document by default otherwise it won't get indexed. I had to change the field name to id so that it translates to _id on MongoDB with the #Id annotation. To fix this on the consumer side, I used #JsonProperty("username") to get the value from the username property of the request's JSON body.

resource link rel with id spring data rest

Having following Entities and Repositories. I could'nt manage to put the id on my relationship. Thanks in advance for your help
Related artifacts from my build.gradle (using Spring Boot version 1.5.4.RELEASE)
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-data-rest')
Entities
Store
#Entity
#Table(name = "store")
class Store {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
long id
String description
#ManyToOne(optional=false, fetch = FetchType.EAGER)
Province province
}
Province
#Entity
#Table(name = "province")
class Province {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
long id
#NotNull
String name
}
Repositories
Store
#RepositoryRestResource(collectionResourceRel = "stores", path = "stores")
interface StoreRepository extends PagingAndSortingRepository<Store, Long> {
}
Province
#RepositoryRestResource(collectionResourceRel = "provinces", path = "provinces")
interface ProvinceRepository extends PagingAndSortingRepository<Province, Long> {
}
Expected Result
I'm expecting this result, please note link on Province
{
"stores": [
{
"id": 1,
"description": "desc1",
"_links": {
"self": {
"href": "http://localhost:8080/stores/1"
},
"store": {
"href": "http://localhost:8080/stores/1"
},
"province": {
"href": "http://localhost:8080/stores/1/province/1" ==> Expecting the url with provinceID since its many to one
}
}
}
]
//Simplified for simplicity sake
}
Actual Result
Not having the Province Id in href
{
"stores": [
{
"id": 1,
"description": "desc1",
"_links": {
"self": {
"href": "http://localhost:8080/stores/1"
},
"store": {
"href": "http://localhost:8080/stores/1"
},
"province": {
"href": "http://localhost:8080/stores/1/province" ==> //NO ID!
}
}
}
]
//Simplified for simplicity sake
}
Basically im expecting
this
"province": {
"href": "http://localhost:8080/stores/1/province/1" ==> Expecting the url with provinceID since its many to one
}
instead of this
"province": {
"href": "http://localhost:8080/stores/1/province" //NO province ID
}
Edit Jun 21 at 11:54
I've changed FetchType.LAZY to EAGER on Store due to an error when trying to do
"http://localhost:8080/stores/1/province/1"
GOT
"org.springframework.http.converter.HttpMessageNotWritableException",
"message": "Could not write JSON: No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer
Finally i've found two possibles ways to solve this one with Projections and the other using ResourceProcessor. Hope this help somebody.
Projections.
#Projection(name="storeProjection", types={Store.class})
interface StoreProjection {
long getId();
String getDescription();
String getPhoneNumber();
StoreStatus getStatus();
#Value("#{target.province.id}")
Long getProvinceId();
#Value("#{target.province.name}")
String getProvinceName();
}
GET http://localhost:8080/stores?projection=storeProjection
JSON result
{
//Store data...
"provinceId": "1"
"provinceName": "Prov1"
//Links,etc data
}
ResourceProcessor in order to add a new link with desired info
#Configuration
class ResourcesProcessors {
#Autowired
EntityLinks entityLinks
#Bean
public ResourceProcessor<Resource<Store>> storeProcessor() {
return new ResourceProcessor<Resource<Store>>() {
#Override
public Resource<Store> process(Resource<Store> resource) { //Este punto solo se agregan nuevos links
Store store = resource.getContent()
Link link = entityLinks.linkToSingleResource(Province.class, store.province.id);
resource.add(link);
return resource;
}
};
}
}

How to consume _embedded resources with Spring HATEOAS

I am trying to consume the following REST HAL response from a 3rd party service:
{
"id": 51780,
"name": "Lambeth",
"description": "",
"address_id": 54225,
"website": "",
"numeric_widget_id": 3602008,
"currency_code": "GBP",
"timezone": "Europe/London",
"country_code": "gb",
"live": true,
"_embedded": {
"settings": {
"has_services": true,
"has_classes": true,
"payment_tax": 0,
"currency": "GBP",
"requires_login": false,
"has_wallets": false,
"ask_address": true,
"_links": {
"self": {
"href": "https://myhost.com/api/v1/51780/settings"
}
}
}
},
"_links": {
"self": {
"href": "https://myhost.com/api/v1/company/51780"
},
"settings": {
"href": "https://myhost.com/api/v1/51780/settings"
}
}
}
Which I would like to map to a class like this:
public class Company extends ResourceSupport {
private String name;
private CompanySettings settings;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public CompanySettings getSettings() {
return settings;
}
public void setSettings(CompanySettings settings) {
this.settings = settings;
}
}
And a class for the embedded item like this:
public class CompanySettings extends ResourceSupport {
private String currency;
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
}
However I am having no luck getting the embedded item to map to the nested settings object. My code is below.
RestTemplate restTemplate = new RestTemplate();
HttpEntity<String> entity = new HttpEntity<String>("parameters", headers);
ResponseEntity<Resource<Company>> responseEntity = restTemplate.exchange("https://uk.bookingbug.com/api/v1/company/51780",
HttpMethod.GET, null, new ParameterizedTypeReference<Resource<Company>>() {
}, Collections.emptyMap());
if (responseEntity.getStatusCode() == HttpStatus.OK) {
Resource<Company> userResource = responseEntity.getBody();
Company company = userResource.getContent();
}
Any help would be greatly appreciated.

Resources