0

I'm working on explore applications of Entity Graphs. When I tried to create few graphs via @NamedEntityGraphs for one entity I figured out, that for each graph I needed a separate method in my repository. So I decided to load data with help of EntityManager and its find method, instead of repository usage. But when I rewrote code, it provide different behavior in some cases. So I make a simple example, to demonstrate it. I have two entities. The Profile and the Post. Each Profile may create Post or add existed Post to its favorites:

//Profile.java
@Entity
@NamedEntityGraphs({
        @NamedEntityGraph(
                name = "profile.post-fav-posts",
                attributeNodes = {
                        @NamedAttributeNode(value = "posts"),
                        @NamedAttributeNode(value = "favoritePosts")
                }
        )
})
public class Profile {
    @Id
    @GeneratedValue
    Long id;
    @ManyToMany
    Set<Post> favoritePosts = new HashSet<>();
    @OneToMany(mappedBy = "owner", fetch = FetchType.LAZY)
    Set<Post> posts = new HashSet<>();

    //... constructors
}

//Post.java
@Entity
public class Post{
    @Id()
    @GeneratedValue()
    Long id;
    String title;
    @ManyToOne
    Profile owner;

    //... constructors
}

I have a repositories for each entity:

//ProfileRepository.java
@Repository
interface ProfileRepository extends JpaRepository<Profile, Long> {

    @Query("SELECT p FROM Profile p WHERE p.id = :id")
    @EntityGraph(value = "profile.post-fav-posts", type = EntityGraph.EntityGraphType.LOAD)
    Profile findByIdWithPostsAndFavoriteProfiles(Long id);
}

//PostRepository .java
@Repository
interface PostRepository extends JpaRepository<Post, Long> {}

And a service for Entity manager usage:

@Service
public class ProfileService {

    @PersistenceContext
    EntityManager entityManager;
    @Autowired
    ProfileRepository profileRepository;

    Profile findByIdWithEntityManager(Long id) {
        Map<String, Object> properties = new HashMap<>();
        properties.put(QueryHints.HINT_LOADGRAPH, entityManager.getEntityGraph("profile.post-fav-posts"));

        return entityManager.find(Profile.class, id, properties);
    }

    Profile findByIdWithRepository(Long id) {
        return profileRepository.findByIdWithPostsAndFavoriteProfiles(id);
    }
}

Finally, I wrote a test to demonstrate the difference in behavior:

@SpringBootTest
public class SimpleTest {

    @Autowired
    ProfileService profileService;
    @Autowired
    ProfileRepository profileRepository;
    @Autowired
    PostRepository postRepository;
    @Autowired
    private PlatformTransactionManager transactionManager;
    private TransactionTemplate transactionTemplate;

    @BeforeEach
    void initTransactionTemplate() {
        transactionTemplate = new TransactionTemplate(transactionManager);
    }

    @Test
    void testEntityManager() {
        Profile profile1 = profileRepository.save(new Profile(1L));
        Profile profile2 = profileRepository.save(new Profile(2L));
        Post post1 =transactionTemplate.execute(status -> postRepository.save(new Post(3L, "Title 1", profile1)));
        Post post2 = transactionTemplate.execute(status -> postRepository.save(new Post(4L, "Title 2", profile2)));
        profile1.favoritePosts.add(post2);
        profileRepository.save(profile1);

        Profile result = profileService.findByIdWithEntityManager(profile1.id);

        System.out.println("Favorite profiles set size: " + result.favoritePosts.size());
        System.out.println("Posts set size: " + result.posts.size());
    }

    @Test
    void testJpaRepository() {
        Profile profile1 = profileRepository.save(new Profile(1L));
        Profile profile2 = profileRepository.save(new Profile(2L));
        Post post1 = transactionTemplate.execute(status -> postRepository.save(new Post(3L, "Title 1", profile1)));
        Post post2 = transactionTemplate.execute(status -> postRepository.save(new Post(4L, "Title 2", profile2)));
        profile1.favoritePosts.add(post2);
        profileRepository.save(profile1);

        Profile result = profileService.findByIdWithRepository(profile1.id);

        System.out.println("Favorite profiles set size: " + result.favoritePosts.size());
        System.out.println("Posts set size: " + result.posts.size());
    }
}

The SQL queries generated after calling find methods:

--Entity manager test:
select 
    profile0_.id as id1_1_0_, 
    favoritepo1_.profile_id as profile_1_2_1_, 
    post2_.id as favorite2_2_1_, 
    post2_.id as id1_0_2_, 
    post2_.owner_id as owner_id3_0_2_, 
    post2_.title as title2_0_2_, 
    profile3_.id as id1_1_3_ 
from profile profile0_ 
    left outer join profile_favorite_posts favoritepo1_ 
    on profile0_.id=favoritepo1_.profile_id 
    left outer join post post2_ 
    on favoritepo1_.favorite_posts_id=post2_.id 
    left outer join profile profile3_ 
    on post2_.owner_id=profile3_.id 
where profile0_.id=?

--Repository test:
select 
    profile0_.id as id1_1_0_, 
    posts1_.id as id1_0_1_, 
    post3_.id as id1_0_2_, 
    posts1_.owner_id as owner_id3_0_1_, 
    posts1_.title as title2_0_1_, 
    posts1_.owner_id as owner_id3_0_0__, 
    posts1_.id as id1_0_0__, 
    post3_.owner_id as owner_id3_0_2_, 
    post3_.title as title2_0_2_, 
    favoritepo2_.profile_id as profile_1_2_1__, 
    favoritepo2_.favorite_posts_id as favorite2_2_1__ 
from profile profile0_ 
    left outer join post posts1_ 
    on profile0_.id=posts1_.owner_id 
    left outer join profile_favorite_posts favoritepo2_ 
    on profile0_.id=favoritepo2_.profile_id 
    left outer join post post3_ 
    on favoritepo2_.favorite_posts_id=post3_.id 
where profile0_.id=?

select 
    profile0_.id as id1_1_0_ 
from profile profile0_ 
where profile0_.id=?

So the questions is:

  1. Why doesn't EntityManager initialize one of relationships (Prodile.posts)?
  2. Is there any solutions to fetch data via entity graphs without annotations?
NotDmitry
  • 1
  • 1
  • Your test seems broken, since it creates two `Post` instances with identical id. For some reason this doesn't blow up. You probably specify proper transactional boundaries. I recommend `TransactionTemplate` for this. Related: do you see the same behaviour when you run the test on individually? And finally please post the SQL statements executed by the tests. – Jens Schauder Aug 24 '22 at 09:08
  • @JensSchauder thank you for your advise. I've fixed ids in tests and added `TransactionTemplate`. Maybe tests've worked even with the same ids because I used `@GeneratedValue`. Also I added SQL queries which are generated when I call `find*` methods. Finally, I executed tests individually and together, in both cases the first one has failed and the second one has passed. – NotDmitry Aug 24 '22 at 18:43

0 Answers0