I am trying to understand how spring data converts my POJO to the API params/body and return type. My goal is to have a save endpoint that takes in non generated fields only, but returns all fields. As an example, consider the following class:
#Node
public class Person {
#Id #GeneratedValue(generatorClass = UUIDStringGenerator.class) private String id;
private String firstname, lastname;
private String zip;
}
with acommpanying repository
public interface PersonRepository extends CrudRepository<Person, String> {}
I don't need the POST /persons endpoint to accept id or zip, only firstName and lastName. However I would like it to return all fields on a successful save.
The first thing I noticed is that if I:
Add getters, but not setters for id and zip and
Add getters and setters for firstName and lastName
The generated open api docs request body has this format
{
"id": "string",
"firstname": "string",
"lastname": "string",
"zip": "string"
}
and if i set id or zip in the request, it will somehow set the properties in the DB, despite both fields being private and having no setters. How is this possible?
I came up with this solution, which works, but it is reliant on id and zip being included in the constructor args but intentionally being ignored in the method body.
#Node
#Setter
#Getter
public class Person {
#Id #GeneratedValue(generatorClass = UUIDStringGenerator.class) private String id;
private String firstname, lastname;
private String zip;
Person(String id, String firstname, String lastname, String zip) {
// If I uncomment the below line, it will override the id with the one passed in.
// this.id = id;
this.firstname = firstname;
this.lastname = lastname;
}
}
This solution works because of the object population hierarchy, it will default to using my all args constructor. But it is not ideal because:
I could easily make a mistake and allow end users to set a custom id.
The API docs request body still asks for id and zip, but setting them now just does nothing
The lack of a no args constructor means the find method fails to cast the result back into a Person object
What is the recommended way to have my api only accept a subset of fields on create, ignoring those that I do not supply when writing to the data store, and then return a different subset of fields as the response. Are projections the recommended way to handle custom responses on default CrudRepository methods?
NB: I am aware that I need to use config.exposeIdsFor(Person.class); and am doing so. I am also awa
Related
I'm trying to display a table listing the Country codes (iso3166) in a postgresql db onto an html page using Spring Boot and Angular, the parameter name in the http response lists "number" when instead I want it to list "nbr".
The SQL table has 4 columns
name (varchar) unique
alpha2 (varchar) PK unique
alpha3 (varchar) unique
nbr (int4)
My Spring Boot Models is the following:
#Entity
#Table(name = "iso3166")
public class Country {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private String alpha2;
#Column(name = "name")
private String name;
#Column(name = "alpha3")
private String alpha3;
#Column(name = "nbr")
private int nbr;
public Country()
{
}
public Country(String name, String alpha2, String alpha3, int nbr)
{
this.name = name;
this.alpha2 = alpha2;
this.alpha3 = alpha3;
this.nbr = nbr;
}
/*
getters and settings + toString()
*/
The repository uses JPARepository
public interface ICountryRepository extends JpaRepository<Country, String> {
}
And the Controller has only the findAll() method
#RestController
#RequestMapping({"/api"})
public class CountryController {
#Autowired
ICountryRepository countryRepository;
#GetMapping
public List<Country> findAll(){
List<Country> country = countryRepository.findAll();
return country;
}
}
Running spring-boot and opening localhost in chrome, the table shows up just fine.
However, looking at the Response tab under Network, it shows up like this
Shows the same thing if I go to http://localhost:8080/api
[{"alpha2":"AX","name":"AALAND ISLANDS","alpha3":"ALA","number":248},{"alpha2":"AF","name":"AFGHANISTAN","alpha3":"AFG","number":4},{"alpha2":"AL","name":"ALBANIA","alpha3":"ALB","number":8},{"alpha2":"DZ","name":"ALGERIA","alpha3":"DZA","number":12},{"alpha2":"AS","name":"AMERICAN SAMOA","alpha3":"ASM","number":16},{"alpha2":"AD","name":"ANDORRA","alpha3":"AND","number":20},{"alpha2":"AO","name":"ANGOLA","alpha3":"AGO","number":24},
Why does the Http Response return the "nbr" field as "number" instead? And how can I change it to show up as "nbr" in the Http response? Does something happen in the background in Spring Boot when formulating the http response that I can't control?
It is a number because you defined it as a number here, in Country entity:
#Column(name = "nbr")
private int nbr;
The best solution is to create another object which is used for HTTP communication. For example: CountryDTO.
In CountryDTO you can define nbr field as String.
Then you just have to create a mapping between Country and CountryDTO objects.
Why you should always do like this:
You should never send Entities directly to the client.
It keeps your code clean and separated: One object is responsible for holding the database model, and another is responsible for communicating with the client. It is now the same, and it is just pure luck.
Found out what happened, although I don't know the specifics.
spring uses Jackson to serialize, and Jackson uses by default public getters to serialize and name data.
Since I named the nbr getter/setters as getNumber() and setNumber, changing it to getNbr() and setNbr() respectively solved the issue.
I'm building a rest API using Spring Boot rest services.
I have a Java class:
class Person{
int id;
#notNull
String name;
#notNull
String password;
}
And I want to make an API to create a Person object. I will recieve a POST request with json body like:
{
"name":"Ahmad",
"password":"myPass",
"shouldSendEmail":1
}
As you can see there are an extra field "shouldSendEmail" that I have to use it to know if should I send an email or not after I create the Person Object.
I am using the following API:
#RequestMapping(value = "/AddPerson", method = RequestMethod.POST)
public String savePerson(
#Valid #RequestBody Person person) {
personRepository.insert(person);
// Here I want to know if I should send an email or Not
return "success";
}
Is there a method to access the value of "shouldSendEmail" while I using the autoMapping in this way?
There's many options for you solve. Since you don't want to persist the shouldSendEmail flag and it's ok to add into you domain class, you can use the #Transient annotation to tell JPA to skip the persistence.
#Entity
public class Person {
#Id
private Integer id;
#NotNull
private String name;
#NotNull
private String password;
#Transient
private Boolean shouldSendEmail;
}
If you want more flexible entity personalizations, I recommend using DTO`s.
MapStruct is a good library to handle DTO`s
You will need an intermediary DTO, or you will otherwise have to modify person to include a field for shouldSendEmail. If that is not possible, the only other alternative is to use JsonNode and manually select the properties from the tree.
For example,
#Getter
public class PersonDTO {
private final String name;
private final String password;
private final Integer shouldSendEmail;
#JsonCreator
public PersonDTO(
#JsonProperty("name") final String name,
#JsonProperty("password") final String password,
#JsonProperty("shouldSendEmail") final Integer shouldSendEmail
) {
this.name = name;
this.password = password;
this.shouldSendEmail = shouldSendEmail;
}
}
You can use #RequestBody and #RequestParam together as following
.../addPerson?sendEmail=true
So send the “sendEmail” value as request param and person as request body
Spring MVC - Why not able to use #RequestBody and #RequestParam together
You have mutli solutions
1 - You can put #Column(insertable=false, updatable=false) above this property
2 - send it as request param #RequestParam
#RequestMapping(value = "/AddPerson", method = RequestMethod.POST)
public String savePerson(
#Valid #RequestBody Person person, #RequestParam boolean sendMail) {}
3- use DTO lets say PersonModel and map it to Person before save
My spring-data-rest integration test fails for a simple json request. Consider the below jpa models
Order.java
public class Order {
#Id #GeneratedValue//
private Long id;
#ManyToOne(fetch = FetchType.LAZY)//
private Person creator;
private String type;
public Order(Person creator) {
this.creator = creator;
}
// getters and setters
}
Person.java
ic class Person {
#Id #GeneratedValue private Long id;
#Description("A person's first name") //
private String firstName;
#Description("A person's last name") //
private String lastName;
#Description("A person's siblings") //
#ManyToMany //
private List<Person> siblings = new ArrayList<Person>();
#ManyToOne //
private Person father;
#Description("Timestamp this person object was created") //
private Date created;
#JsonIgnore //
private int age;
private int height, weight;
private Gender gender;
// ... getters and setters
}
In my test I created a person by using personRepository and inited order by passing person
Person creator = new Person();
creator.setFirstName("Joe");
creator.setLastName("Keith");
created.setCreated(new Date());
created.setAge("30");
creator = personRepository.save(creator);
Order order = new Order(creator);
String orderJson = new ObjectMapper().writeValueAsString(order);
mockMvc.perform(post("/orders").content(orderJson).andDoPrint());
Order is created but creator is not associated with the order. Also I want to pass request body as a json object. In this my json object should contain creator as follows
{
"type": "1",
"creator": {
"id": 1,
"firstName": "Joe",
"lastName": "Keith",
"age": 30
}
}
If I send request body with the following json, the call works fine
{
"type": "1",
"creator": "http://localhost/people/1"
}
But I don't want to send the second json. Any idea how to solve the issue. Because already my client is consuming the server response by sending first json. Now I migrated my server to use spring-data-rest. After that all my client code is not working.
How to solve this?
You are correctly associating order with the creator, however the Person is not associated with the orders. You are missing the List<Order> orders field in Person class. Add this, add annotations, add methods for adding order to person and then before sending JSON you should call something like this:
creator.addOrder(order);
order.setCreator(cretr);
Did you try using cascade = CascadeType.ALL in #ManyToOne annotation
public class Order {
#Id #GeneratedValue//
private Long id;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)//
private Person creator;
private String type;
public Order(Person creator) {
this.creator = creator;
}
// getters and setters
}
Both your Order and Person classes should implement Serializable to properly break them down into and rebuild them from JSON.
There are some ways to solve your problem, but I want give you a hint. You just can save only "id" of your person and get the person by "id" from your database, when you need this.
It solves your problem and it also saves the memory.
I believe you need to do two things to get this work.
Handle the deserialization properly. As you expect Jackson to populate the nested Person object via the constructor you need to annotate this with #JsonCreator. See here:
http://www.cowtowncoder.com/blog/archives/2011/07/entry_457.html
One of more powerful features of Jackson is its ability to use arbitrary >constructors for creating POJO instances, by indicating constructor to use with
#JsonCreator annotation
...........................................
Property-based creators are typically used to pass one or more
obligatory parameters into constructor (either directly or via factory
method). If a property is not found from JSON, null is passed instead
(or, in case of primitives, so-called default value; 0 for ints and so
on).
See also here on why Jackson may not be able to automatically work this out.
https://stackoverflow.com/a/22013603/1356423
Update your JPA mappings. If the associated Person is now populated correctly by the Jackson deserializer then by adding the necessary JPA cascade options to the relationship then both instances should be persisted.
I think then the following should work as expected:
public class Order {
#Id
#GeneratedValue(...)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, cascade = cascadeType.ALL)
private Person creator;
private String type;
#JsonCreator
public Order(#JsonProperty("creator") Person creator) {
this.creator = creator;
}
}
I have the following classes:
#Entity(value="students", noClassnameStored=true)
public class Student {
#Id
private String studentId;
private String firstName;
private String lastName;
private Address address;
}
public class Address {
private String street;
private String city;
private Integer zip;
private String state;
}
When I save the instances of the Student class, I want them to be saved in the following format inside the Mongo database:
{
_id: "12345",
firstName: "Cler",
lastName: "Fit",
street: "123 xyz"
city: "unnt",
zip: 76443
state: "IM"
}
In other words, even though the Java object being saved has a nested member, I want it to be saved as a flat structure in the resulting document. Can I do that in Morphia? I am aware I can do it by defining a custom converter on the "Student" class. But I have way too many fields in that class than I have shown above, and I don't want to individually handle every one of those. Ideally I want a custom converter defined on the "Address" class which can hopefully accomplish the same result.
Tried searching through the Morphia API documentation. Unfortunately nothing much is mentioned in the API documentation.
Morphia does not currently support flattened documents like that, no. You might be able to coerce that with the #Pre- and #Post- annotations to massage data in and out of that shape, but that's a lot of manual mapping.
I have been playing around with Spring Data and MongoDB and have a question about limiting the amount of data for certain queries. I've seen adding custom queries within the MongoRepository, however I haven't seen any examples of limiting the amount of data and returning classes that are basically a subset of larger classes.
For instance I have a User class that has several fields, but I also want to create a UserShort class that has a subset of the fields in the User class. For instance UserShort would only contain the id and firstName / lastName / email fields, rather than everything.
I've seen I can specify/limit the fields that are returned, but can I have those returned into a different class? At the moment the UserShort will return null unless I specify the User class instead, but the fields will be limited to the ones I specify. Wasn't sure if this is possible at all? I realize the User class below isn't huge, but it's the concept I'm after.
A user interface:
public interface Users {}
Subset class:
public class UserShort implements Users {
#Id
private String id;
private String firstName;
private String lastName;
#Indexed(unique = true)
private String email;
//getters / setters
}
Full class:
#Document
public class User implements Users {
#Id
private String id;
private String firstName;
private String lastName;
private String username;
private String password;
private Date dob;
private Status status;
#Indexed(unique = true)
private String email;
private Gender gender;
private String locale;
private Date registerDate;
#DBRef
private List<UserAddress> addresses;
public User(){
addresses = new ArrayList<UserAddress>();
}
//getters / setters
}
And the repository interface:
public interface UserRepository extends MongoRepository<Users, String> {
public User findByEmail(String email);
#Query(value="{ 'email' : ?0 }", fields="{ 'firstName' : 1, 'lastName' : 1}")
public UserShort findUserShortByEmail(String email);
}
As long as the return type of the query method is assignable to the managed domain type (Users in your case) we will prefer the return type to determine the collection to run the query against. Thus, in your case we'd execute the query against userShort instead of users which is why you do not get any results. That behavior is in place to support storing inheritance hierarchies into different collections.
If you switched to User as the domain type for the repository, things should work exactly as expected. This would also have the benefit of preventing clients from handing UserShort instances to the save(…) method which will wipe out properties contained in User but not in UserShort. Here's the final repository interface declaration.
interface UserRepository extends MongoRepository<User, String> {
User findByEmail(String email);
#Query(value="{ 'email' : ?0 }", fields="{ 'firstName' : 1, 'lastName' : 1}")
UserShort findUserShortByEmail(String email);
}
P.S.: #byte-crunch outlined in the comments that this currently only works for collections of projections but not for returning single instances. This has been reported and fixed in DATAMONGO-1030 and will be available in 1.5.4 and 1.6.0 GA.