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:
- Why doesn't EntityManager initialize one of relationships (
Prodile.posts
)? - Is there any solutions to fetch data via entity graphs without annotations?