4

Issue:

I have User and Role entities and the relationship is many to many. First I do a roleRepository.findById() to get a role and then I do role.getUsers().forEach(user -> System.out.println(user.getId())); to print the ids of associated users.

When the first method is invoked, query is issued to role table. And when the second method is invoked, a join query is issued to role_users and user tables.

Is it possible to let hibernate know via any annotation that it should create the Role object with a set of Proxy user objects so that during the above two methods User table is never used?

For example, I can annotate the collection association with @LazyCollection(EXTRA) and then role.getUsers().size() works perfectly without using the user table.

Code

    @Entity
    public class Role {

      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Access(AccessType.PROPERTY)
      private Long id;

      private String name;

      @ManyToMany
      private Set<User> users;
      
      ... getters and setters
    }
    @Entity
    public class User {

      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Access(AccessType.PROPERTY)
      private Long id;

      private String name;

      @ManyToMany(mappedBy = "users")
      private Set<Role> roles;
      
      ..getters and setters
    }
    public interface UserRepository 
        extends JpaRepository<User, Long> {
    }
    public interface RoleRepository 
        extends JpaRepository<Role, Long> {
    
      Role findByName(String roleName);
    }
    @Service
    public class UserService {

      @Autowired
      private UserRepository userRepository;

      @Autowired
      private RoleRepository roleRepository;

      @Transactional
      public void setUpSomeUsersAndRoles(){
        User userOne = userRepository.save(new User("user-1"));
        User userTwo = userRepository.save(new User("user-2"));
        User userThree = userRepository.save(new User("user-3"));

        Role roleOne = roleRepository.save(new Role("ADMIN"));

        userOne.setRoles(singleton(roleOne));
        userTwo.setRoles(singleton(roleOne));
        userThree.setRoles(singleton(roleOne));

        Set<User> users 
                = new HashSet<>(asList(userOne, userTwo, userThree));
        roleOne.setUsers(users);
      }

      @Transactional
      public void findRoleByName(){
        Role role = roleRepository.findByName("ADMIN");

        //I want the following to be executed
        //without query issued to user table
        role.getUsers()
           .forEach(user -> System.out.println(user.getId()));
      }
    }

Note

I do know how to get ids of associated entities via separate query. This is specially a question about the possibility of hibernate annotation as highlighted in the question.

  • 1
    I don't think it's possible to do whay you want. You can write a JPQL query to retrieve those IDs. – Augusto Aug 02 '20 at 19:54
  • 1
    I also think it's not possible, if you need id only better to write a JPQL query for a list of ids only. – Eklavya Aug 02 '20 at 20:20
  • I was thinking the same but I can see it works for `ManyToOne` and for the above, `role.getUsers().size()` works with `@LazyCollection(EXTRA)` that swayed my mind. Ands more it is logically possible because all the related data is in `role` and `role_users` table so it was question of whether hibernate implementation supports it – Kavithakaran Kanapathippillai Aug 02 '20 at 20:25
  • Ok, I am not aware of that for `ManyToOne` it works! How it works for `ManyToOne` ? I tried but it not work for me. – Eklavya Aug 02 '20 at 20:28
  • 1
    See this one. If the access type is `property` for `Id`, you can do the `getId()` https://stackoverflow.com/questions/35013661/lazyinitializationexception-on-getid-of-a-manytoone-reference – Kavithakaran Kanapathippillai Aug 02 '20 at 20:36

2 Answers2

1

Getting the collection size from a proxied collection object indeed gives you the correct cardinality, because it's basically the size of the collection. But as soon as you need the values, queries will be fired off.

There is no special annotation for this. But it is possible to do it yourself. You would have to write your own Lazy Initializer. (Take a look at org.hibernate.proxy package in Hibernate Core source code) It will be messy and probably not worth it.

Your best bet is to use JPQL or Native, something like this:

public interface RoleRepository extends JpaRepository<Role, Long> {

    Role findByName(String roleName);

    @Query(value="SELECT users_id FROM role_users WHERE roles_id = ?1", nativeQuery = true)
    List<Long> findUserIdsForRole(Long roleId);
}
raminr
  • 784
  • 3
  • 12
  • Thank you for your time but I do know how to get the ids via separate query. My question is as specifically stated about any annotation. Because for `ManyToOne` hibernate allows to create a proxy where you can get the id without initialising the proxy – Kavithakaran Kanapathippillai Aug 03 '20 at 06:32
0

Theoretically this can be done with a load-graph but I doubt this will work without bytecode enhancement and even then, I'm not sure if this extra lazyness works in all cases.

Having said that, this is a perfect use case for Blaze-Persistence Entity Views.

Blaze-Persistence is a query builder on top of JPA which supports many of the advanced DBMS features on top of the JPA model. I created Entity Views on top of it to allow easy mapping between JPA models and custom interface defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure the way you like and map attributes(getters) via JPQL expressions to the entity model. Since the attribute name is used as default mapping, you mostly don't need explicit mappings as 80% of the use cases is to have DTOs that are a subset of the entity model.

A DTO mapping for your model could look as simple as the following

@EntityView(User.class)
interface UserDto {
    Integer getId();
}
@EntityView(Role.class)
interface RoleDto {
    Integer getId();
    String getName();
    Set<UserDto> getUsers();
}

Querying is a matter of applying the entity view to a query, the simplest being just a query by id.

RoleDto dto = entityViewManager.find(entityManager, RoleDto.class, id);

But the Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

It will only fetch the mappings that you tell it to fetch

Christian Beikov
  • 15,141
  • 2
  • 32
  • 58