3

I have the following entities, and a CrudRepository for each:

@Entity
class Movie {
    @Id Long id;
    @Column String name;
    @ManyToOne Person director;
}

@Entity
class Person {
    @Id Long id;
    @Column String name;
}

My controller looks like this:

@RestController
@RequestMapping("/movies")
class MovieController {

    private MovieRepository movies = ...;
    private PersonRepository people = ...;

    @PostMapping
    public Movie create(@RequestBody MovieRequest request) {
        // Get the director
        Person director = people.findById(request.directorId);

        // Create the new movie
        Movie movie = new Movie();
        movie.name = request.name;
        movie.director = director;

       // Save the new movie
       return movies.save(movie);
    }
}

class MovieRequest {
    String name;
    Long directorId
}

As you can see, the create method first loads the director by its id, then creates the new movie and finally saves it. This causes two trips to the database: the first one to retrieve the director and the second one to save the movie.

In this case it's not a big problem, but there could be an entity with lots of relations, which means potentially doing lots of queries to achieve a single insert.

Question: I would like to save the new movie in a singe database operation. Is there a way to avoid the initial person query? Is there a better way to handle cases like this?

ESala
  • 6,878
  • 4
  • 34
  • 55
  • 1
    Are you familiar with the difference of findById and getOne ? Since you only need the reference to the director and not the whole object take a look at this question https://stackoverflow.com/q/5482141/7634201 – C. Weber Sep 27 '18 at 15:35
  • thanks @C.Weber that's almost what I want. As you mention the `getOne` method is better than `findById` in this case because it gets a reference instead of the whole entity, but it is still going to the database to check if the id exists and throw `EntityNotFoundException` if it doesn't. I really would like to do this as a single database operation, does that seem possible? – ESala Sep 28 '18 at 08:02
  • You are using Hibernate as JPA Provider? AFAIK Hibernate doesn´t initialize the proxy as long as you don´t access any property other than the identifier. This behaviour can be changed through the following setting hibernate.jpa.compliance.proxy (default should be false). If set to true it will also initialize the proxy when the identifier is accessed. See [Hib User Guide](https://docs.jboss.org/hibernate/stable/orm/userguide/html_single/Hibernate_User_Guide.html#configurations-jpa-compliance) for more info. My guess is you are accessing the proxy,the flag is set to true or my info is old :) – C. Weber Sep 28 '18 at 13:58
  • @ESala what are the columns in your MOVIE table?? – Supun Wijerathne Sep 29 '18 at 09:36
  • @C.Weber yes I'm using Hibernate but not directly, I'm using spring-data-jpa on top. I just tested it with `PersonRepository` extending `JpaRepository` and calling `getOne(id)`, but it still fetches the entity. This was the logged query: `select person0_.id as id1_1_0_, person0_.name as name2_1_0_ from person person0_ where person0_.id=?`. – ESala Sep 29 '18 at 13:46

3 Answers3

1

There's no way to tell the code wich Person it needs to relates to your new Movie. So you really need to perform a query and manually make the association.

There is an alternative that would only be possible if your endpoint create the Person at the same time it creates the Movie. Then you could simply perform the 2 save actions or use a CascadeType=ALL to make a single save action.


If you're able to change your request parameters, might be a good choice to receive a complete Person object instead of accpeting a directorId. This way you could just make the association movie.director = director;.

Be careful with this kinf of approach: if the received Person object is not stored in your database, you'll get an Exception.


Maybe you could create a cache for your Directors. If you have all your Directors saved in a Redis for example, you could search the Director corresponding to the received directorId an then perform the association.

Of course you would still need to make a second operation, but it might be way cheaper than query the database.

andreybleme
  • 689
  • 9
  • 23
  • 1
    Thank you for your answer. Assume the related entities already exist. What happens if I have an entity with 10 relations? Do I need to make 10 queries plus the save? – ESala Sep 26 '18 at 14:12
  • Sadly: yes. The main reason for this is because the `Person` related to your movie depends on the user request parameter. I updated my answer with an an alternative that could be interesting in your particular context to improve performance. – andreybleme Sep 26 '18 at 14:15
  • Thanks for the suggestion of using a cache, but then I'm still waiting on HTTP requests for something that should be a simple insert. Putting the full `Person` in the request is also not acceptable in my case, since the related entity could potentially be large and also have relations. What I'm really looking for is a way to handle this within `spring-data-jpa`. – ESala Sep 26 '18 at 14:30
  • Okay @ESala, in the answer I've tried to include all ideas that came to my mind. Sorry I couldn't help. – andreybleme Sep 26 '18 at 15:36
  • Thank you @andreybleme, I really appreciate it! I'll leave it open for now to see if someone else comes up with another solution. – ESala Sep 26 '18 at 17:38
0

It will be ugly but you have personId in your request, so you can map you Movie with you long personId

class Movie {
    @Id Long id;
    @Column String name;
    @ManyToOne Person director;

    @Column(name="PERSON_ID")
    long personId;
}

in your controller

movie.setPersonId(request.directorId);
Gnk
  • 645
  • 4
  • 11
  • Could you elaborate on that? What do you mean by mapping the movie with the `personId`? – ESala Sep 26 '18 at 14:22
  • Thank you for your answer, but then the director field won't be updated. Also, when doing a query to get the details of a movie, an additional query would be required for each relation. – ESala Sep 26 '18 at 14:37
  • Since the director field will be the same as personId in your table, it will update too. And for details you would be using @ManyToOne Person director – Gnk Sep 26 '18 at 14:44
0

I don't think your MOVIE table contains a 'DIRECTOR_NAME' column (Assuming you follow 2nd rule of normalization). It should be only DIRECTOR_ID.

So you can totally skip loading the director name in your scenario (provided that directId is being sent with the query parameters of the request).

Since you have(should have) a foreign key constraining between Movie.DIRECTOR_ID and DIRECTOR.ID , it will look after any constraint violation if you are trying to insert any DIRECTOR_ID that doesn't exist. So you don't need to worry.

Supun Wijerathne
  • 11,964
  • 10
  • 61
  • 87
  • Yes, this is what I have, the director is referenced as a foreign key `director_id`. What I'm looking for is how to solve the problem described at the end of my question. – ESala Sep 29 '18 at 12:39
  • @ESala you can simply skip that findById query. It's nt needed to save the movie to db? is it? – Supun Wijerathne Sep 29 '18 at 14:14
  • How else would you set the director field when saving the new movie? – ESala Sep 30 '18 at 10:55
  • @ESala pls tell me why you need that? Your MOVIE table doesn't have a column for DIRECTOR_NAME right? So setting up the name to the model is not necessary. – Supun Wijerathne Sep 30 '18 at 10:58