0

I am trying to load an entity from the database with hibernate but I get the following error

failed to lazily initialize a collection of role: package.entities.machinegroup.MachineGroup.users

This is my MachineGroup entity:

public class MachineGroup {
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO, generator = "machine_groups_seq")
    @SequenceGenerator(name = "machine_groups_seq", allocationSize = 1, initialValue = 2)
    @Column(name = "id")
    private long id;
    @Column(name = "name")
    private String name;
    @Column(name = "creation_time")
    private Date creationTime;
    @Column(name = "is_official")
    private boolean official;
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "machine_properties_id", nullable = false)
    private ContinuousIntegrationProperties defaultContinuousIntegrationProperties;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "machine_groups_to_users",
            joinColumns = @JoinColumn(name = "machine_group_id"),
            inverseJoinColumns = @JoinColumn(name = "user_id"))
    private Set<User> users = new HashSet<>();
 @JoinTable(name = "machine_groups_to_versions",
            joinColumns = @JoinColumn(name = "machine_group_id"),
            inverseJoinColumns = @JoinColumn(name = "version_id"))
    private Set<Version> versions = new HashSet<>();
}

This is the entity that I am trying to fetch:

public class MachineGroupToVersion {
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO, generator = "machine_groups_to_versions_seq")
    @SequenceGenerator(name = "machine_groups_to_versions_seq", allocationSize = 1)
    @Column(name = "id")
    private long id;
    @ManyToOne
    @JoinColumn(name = "machine_group_id", nullable = false)
    private MachineGroup machineGroup;
    @ManyToOne
    @JoinColumn(name = "version_id", nullable = false)
    private Version version;
}

Why does it say that it fails lazily initialize the collection users within MachinGroup if I have explicitly said "fetch = FetchType.EAGER" ?

UPDATE:

User class

public class User {
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO, generator = "users_seq")
    @SequenceGenerator(name = "users_seq", allocationSize = 1, initialValue = 2)
    @Column(name = "ID")
    private long id;
    @Column(name = "username")
    private String username;
    @Column(name = "creation_time")
    private Date creationTime;
    @Column(name = "role")
    private String role;
    @Column(name = "email")
    private String email;
    @ManyToMany(mappedBy = "users", cascade = CascadeType.MERGE)
    private Set<MachineGroup> machineGroups = new HashSet<>();
    @OneToMany(mappedBy = "owner")
    private Set<Campaign> campaigns = new HashSet<>();
}

UPDATE 2:

full stacktrace:

Could not write JSON: failed to lazily initialize a collection of role: .entities.user.User.machineGroups, could not initialize proxy - no Session; nested exception is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role: .entities.user.User.machineGroups, could not initialize proxy - no Session (through reference chain: java.util.ArrayList[0]->.entities.machinegrouptoversion.MachineGroupToVersionDTO[\"machineGroup\"]->.entities.machinegroup.MachineGroup[\"users\"]->org.hibernate.collection.internal.PersistentSet[0]->.entities.user.User[\"machineGroups\"])

UPDATE 3:

MachineGroupToVersionService

 @Transactional(transactionManager = "primaryTransactionManager", propagation= Propagation.REQUIRED, readOnly=true, noRollbackFor=Exception.class)
    public List<MachineGroupToVersionDTO> getByMachineGroupName(String mgName) {
        List<MachineGroupToVersionDTO> mgtvl = new ArrayList<>();
        Optional<MachineGroup> mg = machineGroupService.getByName(mgName);
        if(mg.isPresent())
            mgtvl = machineGroupToVersionRepository.findByMachineGroup(mg.get()).stream()
                    .map(this::convertToDto).collect(Collectors.toList());
        return mgtvl;
    }

    private MachineGroupToVersionDTO convertToDto(MachineGroupToVersion mgtv) {
        MachineGroupToVersionDTO machineGroupToVersionDTO = new MachineGroupToVersionDTO();
        machineGroupToVersionDTO.setMachineGroup(mgtv.getMachineGroup());
        machineGroupToVersionDTO.setVersion(mgtv.getVersion());
        machineGroupToVersionDTO.setCreationTime(mgtv.getCreationTime());
        machineGroupToVersionDTO.setState(mgtv.getState());
        machineGroupToVersionDTO.setTestedTime(mgtv.getTestedTime());
        return machineGroupToVersionDTO;

    }
Gilad Dahan
  • 508
  • 5
  • 19

2 Answers2

0

You probably are trying to fetch lazy attributes without oppening a hibernate transaction session (which is required to instantiate lazy collections through hibernate proxy). Add org.springframework.transaction.annotation.Transactional(readOnly = true) annotation to the method which you are using to fetch your MachineGroup collection.

Edit: You're probably facing cyclic fetching issue, while fetching MachineGroup by Name on service method, it fetches the MachineGroup and also, all the users by eager mode (@ManyToMany(fetch = FetchType.EAGER)), which have a List of MachineGroup too.

In your DTO conversion method, you are setting a MachineGroup, that have a list of users, and to set this list of users, each user must have a list of MachineGroup again, which is lazily fetched by default, and that is the exact breakpoint of the thrown exception:

Could not write JSON: failed to lazily initialize a collection of role: .entities.user.User.machineGroups, could not initialize proxy - no Session; nested exception is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role: .entities.user.User.machineGroups, could not initialize proxy - no Session (through reference chain: java.util.ArrayList[0]->.entities.machinegrouptoversion.MachineGroupToVersionDTO[\"machineGroup\"]->.entities.machinegroup.MachineGroup[\"users\"]->org.hibernate.collection.internal.PersistentSet[0]->.entities.user.User[\"machineGroups\"])

If you need to retrieve the information of the many-to-many table machine_groups_to_users I suggest you to create an entity with a composite primary key and remove the List of MachineGroup from your user entity.

  • I am fetching the MachineGroupToVersion entity via its Repository MachineGroupToVersionRepository and as you see MachineGroupToVersion has reference to MachineGroup, I am not explicitly calling a method in the MachineGroup service, it is all done via the MachineGroupToVersion. I added the service to the question also if you can check – Gilad Dahan Aug 12 '21 at 15:27
0

The problem is that in your MachineGroupToVersionDTO the machineGroup your are setting, the MachineGroup#users collection contains at least one User referring to some MachineGroup through User#machineGroups, that is not initialized. You can either try to join fetch all of that or sprinkle some @JsonIgnore annotations around but I doubt this will work for you as you will probably want to serialize the machine groups of a user for some other endpoints. So in the end, the only option you have (in my opinion) is that you introduce DTOs all the way and do not use any entities in your DTO models.

I think this is a perfect use case for Blaze-Persistence Entity Views.

I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.

A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:

@EntityView(MachineGroupToVersion.class)
public interface MachineGroupToVersionDto {
    @IdMapping
    Long getId();
    String getState();
    Date getCreationTime();
    Date getTestedTime();
    VersionDto getVersion();
    MachineGroupDto getMachineGroup();

    @EntityView(Version.class)
    interface VersionDto {
        @IdMapping
        Long getId();
        String getName();
    }
    @EntityView(MachineGroup.class)
    interface MachineGroupDto {
        @IdMapping
        Long getId();
        String getName();
        Date getCreationTime();
        Set<UserDto> getUsers();
    }
    @EntityView(User.class)
    interface UserDto {
        @IdMapping
        Long getId();
        String getUsername();
    }
}

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

MachineGroupToVersionDto a = entityViewManager.find(entityManager, MachineGroupToVersionDto.class, id);

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

Page<MachineGroupToVersionDto> findAll(Pageable pageable);

The best part is, it will only fetch the state that is actually necessary!

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