I'm learning Spring with "Spring in action 5" and faced some problem: when i switch to Spring Data JPA from JDBC (which is 100% working correctly) in chapter 3, the code stops working as i try to open main page with taco's ingredients. I made a few logs to see what's going on and found out that method findById(String id) can't convert my value from DB (or something like that). I'm using MySQL.
I tried to call convertor's method convert(String id) by myself using #Autowired, but the only thing i discovered was that when key is wrong, another error will appear. So data is visible. I will try to provide some code here, but i'm not sure what is useful and what is not. I'm getting error on the first attemp to log something. Errors in IDE and in browser are different.
Here's full project https://github.com/thedistantblue/taco-cloud-jpa.
Here's my converter:
public class IngredientByIdConverter implements Converter<String,
Ingredient> {
private IngredientRepository ingredientRepo;
#Autowired
public IngredientByIdConverter(IngredientRepository ingredientRepo) {
this.ingredientRepo = ingredientRepo;
}
#Override
public Ingredient convert(String id) {
log.info("In converter.convert(): "
+ingredientRepo.findById(id).toString());
Optional<Ingredient> optionalIngredient =
ingredientRepo.findById(id);
return optionalIngredient.orElse(null);
}
}
And controller class:
#Slf4j
#Controller
#RequestMapping("/design")
#SessionAttributes("order")
public class DesignTacoController {
#ModelAttribute(name = "order")
public Order order() {
return new Order();
}
#ModelAttribute(name = "taco")
public Taco taco() {
return new Taco();
}
private final IngredientRepository ingredientRepository;
private TacoRepository designRepository;
private IngredientByIdConverter converter;
#Autowired
public DesignTacoController(IngredientRepository ingredientRepository,
TacoRepository designRepository,
IngredientByIdConverter converter) {
this.ingredientRepository = ingredientRepository;
this.designRepository = designRepository;
this.converter = converter;
}
#GetMapping
public String showDesignForm(Model model) {
List<Ingredient> ingredients = new ArrayList<>();
log.info(converter.convert("CARN").getName());
log.info("in DTC: " + ingredientRepository.findAll());
ingredientRepository.findAll().forEach(i -> ingredients.add(i));
In IDE:
java.lang.NumberFormatException: For input string: "PROTEIN"
at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054) ~[na:na]
at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110) ~[na:na]
at java.base/java.lang.Double.parseDouble(Double.java:543) ~[na:na]
In browser:
There was an unexpected error (type=Internal Server Error, status=500).
For input string: "PROTEIN"; nested exception is java.lang.NumberFormatException: For input string: "PROTEIN"
org.springframework.dao.InvalidDataAccessApiUsageException: For input string: "PROTEIN"; nested exception is java.lang.NumberFormatException: For input string: "PROTEIN"
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:373)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:255)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:527)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
For Ingredient.java need to add annotation #Enumerated(EnumType.STRING) for field Type type
#Id
#NaturalId(mutable = false)
private final String id;
private final String name;
#Enumerated(EnumType.STRING)
private final Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
#Pavel 's solution does work.
I just want to point out that there is another thing, may be worth checking.
This book talks about JDBC before JPA, and it uses data.sql to insert data into table 'Ingredient'. In file data.sql, the type of 'Ingredient.Type', whose value contains 'WRAP', 'PROTEIN', etc. , is String.
However, in the example of JPA, the content of data.sql is moved to TacoCloudApplication, which is wroted in method 'dataLoader', and, in this method, you can see that it just creates Ingredient instance with Ingredient.Type(not a String).
To find the difference between two ways, you can run TacoCloudApplication, then look up Table 'Ingredient'.
The value of Type field is String, if you use the JDBC version example code. The value of Type field is Interger, if you use the JPA version example code.
Related
I'm refactoring my code. I want to use java records instead of java class in my DTO. To convert DTO to Entity, I'm using ModelMapper (version 2.3.5). When I try to get info about user (call method co convert Entity to DTO) I get this error.
Failed to instantiate instance of destination xxx.UserDto. Ensure that xxx.UserDto has a non-private no-argument constructor.
This is my code.
public record UserDto(String firstName,
String lastName,
String email,
String imageUrl) {}
#RestController
public class UserController {
#Autowired
private UserRepository userRepository;
#Autowired
private ModelMapper modelMapper;
#GetMapping("/user/me")
#PreAuthorize("hasRole('USER')")
public UserDto getCurrentUser(#CurrentUser UserPrincipal userPrincipal) {
return convertToDto(userRepository.findById(userPrincipal.getId())
.orElseThrow(() -> new ResourceNotFoundException("User", "id", userPrincipal.getId())));
}
private UserDto convertToDto(User user) {
UserDto userDto = modelMapper.map(user, UserDto.class);
return userDto;
}
private User convertToEntity(UserDto userDto) throws Exception {
User post = modelMapper.map(userDto, User.class);
return post;
}
}
Edit: Updating to version 2.3.8 doesn't help!
The fields of a record are final, so they must be set through the constructor. Many frameworks will cheat and use various tricks to modify final fields after the fact anyway, but these will not work on records. If you want to instantiate a record, you have to provide all the field values at construction time.
It may take a little time for frameworks to learn about records. The old model of "call a no-arg constructor, then set the fields" will not work for records. Some frameworks are already able to deal with this (e.g., "constructor injection"), while others are not yet there. But, we expect that frameworks will get there soon enough.
As the commenters said, you should encourage your framework provider to support them. It isn't hard.
record is a preview feature in Java 14 so I would recommend you to not use it on production. Records were finalized in Java 16. Secondly, it doesn't mimic java bean.
record doesn't have a default no arg constructor implicitly if there are fields. If you wrote a no arg constructor, you will have to delegate the call to all args constructor and since all the fields are final you can only set them once. So you are kind of stuck there. See JEP 359:
It is not a goal to declare "war on boilerplate"; in particular, it is not a goal to address the problems of mutable classes using the JavaBean naming conventions.
An alternative that works today would be to use Lombok. Example of UserDto using Lombok:
#NoArgsConstructor
#AllArgsConstructor
#Data
public class UserDto {
private String firstName;
private String lastName;
private String email;
private String imageUrl;
}
I am currently writing a service where I can store a geospatial point with some data. I have a "dataPoint" class that looks like this:
#Entity
#Table(name = "datapoint")
public class DataPoint {
#Id
int dataPoint_id;
#Column(name = "body")
String body;
#Column(name = "location", columnDefinition = "Geometry")
PGgeometry location;
#Column(name = "deleted")
boolean deleted;
//Getters and Setters...
I am trying to use Spring Boot to simply add a point with some information to a PostGIS database via an API path. I have built a controller that looks like this:
#RestController
#RequestMapping(value = "/dataPoint")
public class DataPointController {
#Autowired
private DataPointService myPointService;
#RequestMapping(value = "/add/{body}/{latitude}/{longitude}/")
public DataPoint addDataPoint(#PathVariable String body, #PathVariable double latitude, #PathVariable double longitude){
DataPoint myPoint = new DataPoint();
myPoint.setBody(body);
PGgeometry geometry = new PGgeometry();
try {
geometry.setValue("POINT("+longitude +" " + latitude+")");
geometry.setType("POINT");
// Debugging Stuff
System.out.println("GEOMETRY VALUE LOOK: {{{{ " + geometry.getValue() + " " + geometry.getType());
} catch (SQLException e) {
e.printStackTrace();
}
myPoint.setLocation(geometry);
myPointService.saveDataPoint(myPoint);
return myPoint;
}
Which is in turn linked to a DataPointService which just acts as a middle man between the controller where saveDataPoint() looks like this:
public void saveDataPoint(DataPoint myPoint) {
dataPointRepository.save(myPoint);
}
and the DataPointRepository, which looks like this:
#Repository
public interface DataPointRepository extends JpaRepository<DataPoint, Integer> {
}
However, when I visit my add link, I get this error:
Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: Direct self-reference leading to cycle (through reference chain: com.testing.model.DataPoint["location"]->org.postgis.PGgeometry["geometry"]->org.postgis.Point["firstPoint"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Direct self-reference leading to cycle (through reference chain: com.testing.model.DataPoint["location"]->org.postgis.PGgeometry["geometry"]->org.postgis.Point["firstPoint"])
I have seen the #JsonBackReference and its dual used in some examples, however, that has been used in situations where entities are being linked back and forth, which I do not see happening here, in fact, the error does not even seem to be cyclic, so what is happening here?
I ran into the same issue. It's cyclic because Point has a field firstPoint that reference to Point again.
I was able to resolve the problem by installing this postgis-geojson: https://jitpack.io/p/stephenbrough/postgis-geojson
I'm currently working on a SpringBoot API to interface with a MongoRepository, but I'm having trouble understanding how the JSON being passed becomes a Document for storage within Mongo. I currently have a simple API that stores a group of users:
#Document
#JsonInclude
public class Group {
#Id
#JsonView(Views.Public.class)
private String id;
#JsonView(Views.Public.class)
private String name;
#JsonView(Views.Public.class)
private Set<GroupMember> groupMembers = new HashSet<>();
}
There are also setter and getter methods for each of the fields, although I don't know how necessary those are either (see questions at the end).
Here is the straightforward component I'm using:
#Component
#Path("/groups")
#Api(value = "/groups", description = "Group REST")
public class Groups {
#Autowired
private GroupService groupService;
#GET
#Produces(MediaType.APPLICATION_JSON)
#ApiOperation(value = "Get all Groups", response = Group.class, responseContainer = "List")
#JsonView(Views.Public.class)
public List<Group> getAllGroups() {
return groupService.getAllGroups();
}
#POST
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
#ApiOperation(value = "Create a Group", response = Group.class)
#JsonView(Views.Detailed.class)
public Group submitGroup(Group group) {
return groupService.addGroup(group);
}
}
Finally, I have a Service class:
#Service
public class GroupServiceImpl implements GroupService {
#Autowired
private GroupRepository groupRepository;
#Override
public Group addGroup(Group group) {
group.setId(null);
return groupRepository.save(group);
}
#Override
public List<Group> getAllGroups() {
return groupRepository.findAll();
}
}
The GroupRespository is simply an interface which extends MongoRepository<Group,String>
Now, when I actually make a call to the POST method, with a body containing:
{
"name": "group001",
"groupMembers": []
}
I see that it properly inserts this group with a random Mongo UUID. However, if I try to insert GroupMember objects inside the list, I receive a null pointer exception. From this, I have two questions:
How does SpringBoot (Jackson?) know which fields to deserialize from the JSON being passed? I tested this after deleting the getter and setter methods, and it still works.
How does SpringBoot handle nested objects, such as the Set inside the class? I tested with List instead of Set, and it worked, but I have no idea why. My guess is that for each object that is both declared in my class and listed in my JSON object, SpringBoot is calling a constructor that it magically created behind the scenes, and one doesn't exist for the Set interface.
Suppose I'm adamant on using Set (the same user shouldn't show up twice anyway). What tools can I use to get SpringBoot to work as expected?
It seems to me that a lot of the things that happen in Spring are very behind-the-scenes, which makes it difficult for me to understand why things work when they do. Not knowing why things work makes it difficult to construct things from scratch, which makes it feel as though I'm hacking together a project rather than actually engineering one. So my last question is something like, is there a guide that explains the wiring behind the scenes?
Finally, this is my first time working with Spring... so please excuse me if my questions are entirely off the mark, but I would appreciate any answers nonetheless.
I have a question about the formatter in Spring.
I have a Formatter for my select boxes, for example:
public class SportTypeFormatter implements Formatter<SportType> {
#Autowired
private SportTypeRepository sportTypeRepository;
#Override
public String print(SportType sportType, Locale locale) {
return String.valueOf(sportType.getTypeId());
}
#Override
public SportType parse(String sportTypeId, Locale locale) throws ParseException {
return sportTypeRepository.findSportTypeByTypeId(Long.valueOf(sportTypeId));
}
}
in thymeleaf something like this:
<select class="form-control" name="sportTypeId" th:field="*{person.sport.sportType}">
<option th:each="spoType : ${allSportTypes}" th:value="${spoType.typeId}" th:selected="${spoType.typeId == person.sport.sportType}" th:text="#{${'login.sport.sportType.' + spo.typeId}}" >Sporttype</option>
</select>
Thats easy, because i only need one value (the id) and i'm going with select-box.
But what is if i need two values?
Suggest i have Email, there i need the id and the value (mail-address). I can build an Formatter for email but i have no chance to transfer the email and id at the same time.
In the print method i can something like that:
#Override
public String print(Email email, Locale locale) {
return email.getId() + email.getEmail();
}
in thymeleaf:
<input id="email" type="text" class="form-control" th:field="*{{person.email}}" th:placeholder="#{login.email}" />
But with this the user can see the id.
If i do the binding the "standard" Spring way i get the following exception (thats the reason why i use the formatter):
{timestamp=Wed Dec 10 11:14:47 CET 2014, status=400, error=Bad Request, exception=org.springframework.validation.BindException,
errors=[Field error in object 'person' on field 'email': rejected value [com.sample.persistence.user.model.Email#0];
codes [typeMismatch.person.email,typeMismatch.person.institutionEmployees.email,
typeMismatch.email,typeMismatch.email,
typeMismatch.email,typeMismatch.com.sample.persistence.user.model.Email,typeMismatch];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [person.email,email];
arguments [];
default message [email]];
default message [Failed to convert property value of type 'com.sample.persistence.user.model.Email'
to required type 'com.sample.persistence.user.model.Email'
for property 'email';
nested exception is org.springframework.core.convert.ConversionFailedException:
Failed to convert from type com.sample.persistence.user.model.Email
to type #javax.persistence.OneToOne #javax.persistence.JoinColumn #com.google.gson.annotations.Expose
com.sample.persistence.user.model.Email for value
'com.sample.persistence.user.model.Email#0';
nested exception is org.springframework.dao.InvalidDataAccessApiUsageException:
Provided id of the wrong type for class com.sample.persistence.user.model.Email.
Expected: class java.lang.Long, got class java.lang.String;
nested exception is java.lang.IllegalArgumentException:
Provided id of the wrong type for class com.sample.persistence.user.model.Email.
Expected: class java.lang.Long, got class java.lang.String]],
message=Validation failed for object='person'. Error count: 1, path=/manageUsers/Ab-Soul/edit}
Any suggestion are welcome.
Thanks in advance.
1. EDIT:
The Controller-method
#RequestMapping(value = "/{login}/edit", method = RequestMethod.GET)
public ModelAndView editUserByLogin(#PathVariable("login") final String login) {
final User currentUser = UserRepository.findPersonByLogin(login);
ModelAndView mav = new ModelAndView(URL_EDIT_USER);
mav.addObject(MODEL_USER, currentUser);
return mav;
}
the scenario, the 'admin' get a list of all current user, if he clicked on the table the requestmapping-method would be called with the name of the user he has clicked.
the email class:
#Entity(name="Email")
public class Email implements Serializable
{
private static final long serialVersionUID = 6891079722082340011L;
#Id()
#GeneratedValue(strategy=GenerationType.AUTO)
#Expose
protected Long emailId;
#Expose
protected String value;
//getter/setter
#Override
public boolean equals(Object obj)
{
if(obj instanceof Email){
return value.equals(((Email) obj).getValue());
}
return false;
}
#Override
public int hashCode()
{
return value.hashCode();
}
}
2. EDIT:
Now i have change the email field of child to emailChild
org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'person' on field 'child[0].emailChild':
rejected value [com.sample.persistence.user.model.Email#0];
codes [typeMismatch.person.child[0].emailChild,
typeMismatch.person.child.emailChild,
typeMismatch.child[0].emailChild,
typeMismatch.child.emailChild,
typeMismatch.emailChild,
typeMismatch.com.sample.persistence.user.model.Email,typeMismatch];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [person.child[0].emailChild,child[0].emailChild];
arguments [];
default message [child[0].emailChild]];
default message [Failed to convert property value of type 'com.sample.persistence.user.model.Email' to required type 'com.sample.persistence.user.model.Email'
for property 'child[0].emailChild';
nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type com.sample.persistence.user.model.Email
to type #javax.persistence.OneToOne #javax.persistence.JoinColumn #com.google.gson.annotations.Expose com.sample.persistence.user.model.Email
for value 'com.sample.persistence.user.model.Email#0';
nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: Provided id of the wrong type for class com.sample.persistence.user.model.Email. Expected: class java.lang.Long, got class java.lang.String;
nested exception is java.lang.IllegalArgumentException: Provided id of the wrong type for class com.sample.persistence.user.model.Email. Expected: class java.lang.Long, got class java.lang.String]
3. EDIT:
Adding the controller method for the post:
#RequestMapping(value = "/{login}/edit", method = RequestMethod.POST)
public ModelAndView updateUser(#PathVariable("login") final String login, #ModelAttribute(MODEL_USER) final Person person, BindingResult bindingResult, final Model model) {
Person repositoryPerson = personRepository.findPersonByLogin(login);
repositoryPerson = repositoryPerson.updateWith(person);
manageUserService.updatePerson(repositoryPerson);
model.asMap().clear();
return new ModelAndView("redirect:" + URL_USERS_OVERVIEW, MODEL, model);
}
I have answered my own question.
We should remember that
the registration process works fine, it would bind fine
Only the Problem occurs by the edit process, so i have a valid Email-Object in the db, bind with a valid Child-Object and this is bind with a valid User-Object.
I have decided to build a EmailFormatter with the following implemenatation:
public class EmailFormatter implements Formatter<Email> {
#Override
public String print(Email email, Locale locale) {
return email.getValue();
}
#Override
public Email parse(String mailAddress, Locale locale) throws ParseException {
return new Email(mailAddress);
}
}
with this, i get in my Child-Instance a new Email-Object (without the id). Now i have a valid Email-Object in a valid Child-Object. Take a look at the post-Method:
#RequestMapping(value = "/{login}/edit", method = RequestMethod.POST)
public ModelAndView updateUser(#PathVariable("login") final String login, #ModelAttribute(MODEL_USER) final Person person, BindingResult bindingResult, final Model model) {
Person repositoryPerson = personRepository.findPersonByLogin(login);
repositoryPerson = repositoryPerson.updateWith(person);
manageUserService.updatePerson(repositoryPerson);
model.asMap().clear();
return new ModelAndView("redirect:" + URL_USERS_OVERVIEW, MODEL, model);
}
At this time i have the login name from the user, i get the "old" (valid) user-Object from the db. I have wrote a "merge" method, i give the "old" user object the modified user object.
Here i can get the old Email Object with id and set the email-adress just like this:
this.emailChild.setValue(modifiedChild.getemailChild().getValue());
After this i look at this solution, i notice thats really simple.
But if the binding problem occurs by a "complexer" (more then one relevant field) object then, i think, you go better white the solution that PatrickLC suggest:
#InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields("emailId");
}
My problem is with having Spring bind the data I get from a form to a JPA entity. The wierd part is, it works just fine if I do not look at the BindingResults. The BindingResults says there were binding errors when an empty string is passed in for the field graduation, but I know it does bind them correctly because when I don't check Hibernate updates the database perfectly. Is there a way to not have to write logic to circumnavigate the wrongly fired binding errors?
#Entity
#Table(name="child")
public class Child {
#Id
#Column(name="id")
private Integer childId;
#ManyToOne(fetch=FetchType.EAGER )
#JoinColumn(name="house", referencedColumnName="house")
private House house;
#NotNull()
#Past()
#Column(name="birthday")
private Date birthday;
#Column(name="graduation_date")
private Date graduationDay;
}
I have tried the following lines in a property editor to no avail
SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy");
registry.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
Here is the method signature for the controller method Handling the request
#Controller
#SessionAttributes(value="child")
#RequestMapping(value="child")
public class ChildModController {
#RequestMapping(value="save-child.do", params="update", method = RequestMethod.POST)
public #ResponseBody Map<String,?> updateChild(
HttpServletRequest request,
#Valid #ModelAttribute(value="child")Child child,
BindingResult results)
}
This is what I get from the BindingResult class as a message
09:01:36.006 [http-thread-pool-28081(5)] INFO simple - Found fieldError: graduationDay,
Failed to convert property value of type java.lang.String to required type java.util.Date for property graduationDay;
nested exception is org.springframework.core.convert.ConversionFailedException:
Failed to convert from type java.lang.String to type #javax.persistence.Column java.util.Date for value ';
nested exception is java.lang.IllegalArgumentException
Spring automatically binds simple object types like String and Number, but for complex objects like java.util.Date or your own defined types, you will need to use what is called a PropertyEditors or Converters, both could solve your problem.
Spring already has a predefiend PropertyEditors and Converters like #NumberFormat and #DateTimeFormat
You can use them directly on your fields like this
public class Child {
#DateTimeFormat(pattern="dd/MM/yyyy")
private Date birthday;
#DateTimeFormat(iso=ISO.DATE)
private Date graduationDay;
#NumberFormat(style = Style.CURRENCY)
private Integer myNumber1;
#NumberFormat(pattern = "###,###")
private Double myNumber2;
}
Spring also allows you to define your own type converters which you must use it combined with Spring ConversionService
For example if you have a Color class like this
public class Color {
private String colorString;
public Color(String color){
this.colorString = color;
}
}
You would define the color converter for example like this
public class StringToColor implements Converter<String, Color> {
public Color convert(String source) {
if(source.equal("red") {
return new Color("red");
}
if(source.equal("green") {
return new Color("green");
}
if(source.equal("blue") {
return new Color("blue");
}
// etc
return null;
}
}
To check more about converters check this, also check this to know the difference between Converters and PropertyEditors