Jon
Jon

Reputation: 161

Adding a Spring Data Repository disables functionality

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

Answers (2)

Oliver Drotbohm
Oliver Drotbohm

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

ledniov
ledniov

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

Related Questions