Reputation: 161
I have these two entities.
@Entity
public class Person {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(cascade=CascadeType.ALL)
private Location location;
public Person() {
}
@Entity
public class Location {
@Id @GeneratedValue
private Long id;
private String place;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "location")
private Set<Person> persons;
public Location() {
}
I also have this Controller.
@Controller
public class PersonController {
private final PersonRepository repo;
public PersonController(PersonRepository repo) {
this.repo = repo;
}
@GetMapping("/")
public String newPerson(Person person){
return "home";
}
@PostMapping("/")
public String newPerson(Person person, BindingResult result){
repo.save(person);
return "redirect:/person";
}
And this Repository.
@Repository
public interface PersonRepository extends JpaRepository<Person, Long> {
Optional<Person> findFirstByName(String name);
}
I also have this backing form.
<form action="#" th:action="@{/}" th:object="${person}" method="post">
<table>
<tr>
<td>Name:</td>
<td><input type="text" th:field="*{name}" /></td>
</tr>
<tr>
<td>Location:</td>
<td><input type="text" th:field="*{location}" /></td>
</tr>
<tr>
<td><button type="submit">Submit</button></td>
</tr>
</table>
</form>
This all works fine when I submit some data. A Person object is saved and so is a Location object.
But when I add
@Repository
public interface LocationRepository extends JpaRepository<Location,
Long> {)
the Location object does not save to the database when I submit the same exact form. Why would just adding this repository cause this issue and what is the solution? Thanks.
Upvotes: 2
Views: 106
Reputation: 83051
To elaborate on why things work as they work:
The form binding uses the ConversionService
. Spring Data registers a conversion chain from String
-> id type -> entity type for each repository managed domain class. So the moment you add a repository, the String
submitted as value for Person.location
will be interpreted as an identifier for an already existing location. It will cause a by-id lookup with the value submitted for the field named location
.
This is handy in the following scenario: assume you're Location
is basically a curated list of instances held in the database, e.g. a list of countries. They you don't want to arbitrarily create new ones but rather select one from the overall list, which basically boils down to having to use a dropdown box instead of a text field.
So conceptually, the fundamental things at odds are the cascades (as they indicate a composition, i.e. Location
being part of the aggregate) and the existence of LocationRepository
as a repository causes the managed type to implicitly becoming an aggregate root (which is fundamental DDD).
This in turn means you have to handle the lifecycle of that instance separately. A potential solution is to inspect the Location
bound to the Person
, check whether an instance with that place
already exists (via a query method on LocationRepository
) and if so, replace the one bound with the one loaded or just call LocationRepository.save(…)
with the original instance to create a new one.
I still don't totally buy that the original attempt created a correct Location
as from your template Spring Framework is not able to guess that what you submit as location
is supposed to be the place
actually. So I assume you saw a Location
instance being created, but completely empty and the BindingResult
actually carrying an error, claiming it couldn't transform the location
form field into an instance of Location
.
Upvotes: 1
Reputation: 2372
You whould fix your form in order to write attribute of location property:
<td><input type="text" th:field="*{location.place}" /></td>
Also you don't have to put @Repository
annotation on your repositories.
Upvotes: 1