Issue fetching data from database using JPA with @OneToMany and @ManyToOne mappings

Current Version: Java version 20.0.1, Spring Boot 3.1.2

Problem Description: I’m encountering an issue when fetching data from the database using JPA. I have two entities, AccountsEntity and InvitationEntity, with a @OneToMany and @ManyToOne relationship.

AccountsEntity:
--------------------

import java.io.Serializable;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Set;

import org.hibernate.annotations.Where;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

import co.cfly.fp.accounts.phonenumber.unassigned.TwilioUnassignedNumberEntity;
import co.cfly.fp.invitation.InvitationEntity;
import co.cfly.fp.profile.common.MembersEntity;
import co.cfly.fp.profile.registeredprofile.RegisteredProfileEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import jakarta.persistence.Version;

@Entity
@Table(name = "tbl_account")
@Where(clause = "is_deleted = false")
public class AccountsEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue (strategy = GenerationType.IDENTITY)
	@Column(name = "tbl_account_id_pk")
	private Long accountId;

	@Column(name = "account_name")
	private String accountName;
	
	@Enumerated(EnumType.STRING)
	@Column(name = "account_type")
	private AccountType accountType;

	@Column(name = "account_image")
	private String accountImage;

	@OneToOne
	@JoinColumn(name = "tbl_app_user_creator_id_fk", referencedColumnName = "tbl_app_user_id_pk")
	private RegisteredProfileEntity appUserCreator;

	@OneToOne
	@JoinColumn(name = "tbl_app_user_owner_id_fk", referencedColumnName = "tbl_app_user_id_pk")
	private RegisteredProfileEntity accountOwner;

	@Column(name = "creation_date_time", columnDefinition = "TIMESTAMP", nullable = false)
	@CreatedDate
	private ZonedDateTime creationDateTime;
	
	@Column(name = "last_modified_date_time", columnDefinition = "TIMESTAMP", nullable = false)
	@LastModifiedDate
	private ZonedDateTime updationDateTime;

	@Column(name = "is_active")
	private boolean isActive;

	@Column(name = "created_by")
	private Long createdBy;

	@Column(name = "updated_by")
	private Long updatedBy;

	@Column(name = "is_deleted")
	private boolean isDeleted;

	@OneToMany(mappedBy = "account")
	private List<MembersEntity> appMembers;
	
	@OneToMany(mappedBy = "account")
	private Set<InvitationEntity> invities;
	
	@OneToMany(mappedBy = "accounts")
	private Set<TwilioUnassignedNumberEntity> twilioUnassignedNumbers;
	
	@Column(name = "version")
	@Version
	private Long version;
	
	//Setters & Getters
	//no arguments constructor
InvitationEntity:
-------------------

import java.io.Serializable;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;

import jakarta.persistence.*;
import org.hibernate.annotations.Where;

import co.cfly.fp.accounts.AccountsEntity;
import co.cfly.fp.call.conference.ConferenceEntity;
import co.cfly.fp.group.GroupEntity;
import co.cfly.fp.nonapp.NonAppUserEntity;
import co.cfly.fp.phonenumber.TwilioPhoneNumberEntity;
import co.cfly.fp.phonenumber.TwilioPhoneNumberType;
import co.cfly.fp.profile.registeredprofile.RegisteredProfileEntity;
import co.cfly.fp.util.common.ModuleType;
import co.cfly.fp.util.common.StatusTypeEnum;

@Entity
@Table(name = "tbl_invitation_app_and_nonapp")
@Where(clause = "is_deleted = false")
public class InvitationEntity implements Serializable {

	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue (strategy = GenerationType.IDENTITY)
	@Column(name = "tbl_invitation_id_pk")
	private Long invitationId;

	@Column(name = "status")
	private StatusTypeEnum statusTypeEnum;

	@Enumerated(EnumType.STRING)
	@Column(name = "module_type")
	private ModuleType moduleType;

	@Column(name = "sms")
	private String sms;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "tbl_account_id_fk")
	private AccountsEntity account;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "tbl_conference_id_fk")
	private ConferenceEntity conference;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "tbl_group_id_fk")
	private GroupEntity group;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "tbl_twilio_phone_number_id_fk")
	private TwilioPhoneNumberEntity twilioPhoneNumberEntity;
	
	@Enumerated(EnumType.STRING)
	@Column(name = "twilio_phone_number_type")
	private TwilioPhoneNumberType twilioPhoneNumberType;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "tbl_app_user_sender_fk")
	private RegisteredProfileEntity sender;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "tbl_app_user_reciever_fk")
	private RegisteredProfileEntity reciever;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "tbl_non_app_user_id_fk")
	private NonAppUserEntity nonAppUser;

	@Column(name = "creation_date_time", columnDefinition = "TIMESTAMP", nullable = false)
	private ZonedDateTime creationDateTime;

	@Column(name = "last_modified_date_time", columnDefinition = "TIMESTAMP", nullable = false)
	private ZonedDateTime updationDateTime;

	@Column(name = "is_active")
	private boolean isActive;

	@Column(name = "created_by")
	private Long createdBy;

	@Column(name = "updated_by")
	private Long updatedBy;

	@Column(name = "is_deleted")
	private boolean isDeleted;
	
	@Column(name = "twilio_sender_reciept")
	private String twilioSenderReciept;
	
	@Column(name = "reinvite_count")
	private int reInviteCount;
	
	//setters & getters
	//no arguments constructor
AccountsServiceImpl:
----------------------------

	public Set<Accounts> findAllByAppUserId() {
		log.info("findAllByAppUserId : method call started");
		UserDetailsImpl userDetails = fetchUserDetailsFromToken();
		String userId = userDetails.getId().toString();
		RegisteredProfileEntity requestingUser = rps.findUser(userId);
		Set<AccountsEntity> accountsEntitiesForMember = requestingUser.getMembers().stream()
				.map(member -> member.getAccount()).filter(account -> null != account && account.isActive())
				.collect(Collectors.toSet());

		Set<AccountsEntity> accountsEntitiesForAdmin = accountsRepo.findAllByAccountOwnerAndIsActiveTrue(requestingUser)
				.orElse(null);
		Set<AccountsEntity> accountsEntitiesForMemberAndAdmin = new HashSet<AccountsEntity>();
		if (!FoneappUtil.isNullOrEmpty(accountsEntitiesForMember))
			accountsEntitiesForMemberAndAdmin.addAll(accountsEntitiesForMember);
		if (!FoneappUtil.isNullOrEmpty(accountsEntitiesForAdmin))
			accountsEntitiesForMemberAndAdmin.addAll(accountsEntitiesForAdmin);
		if (FoneappUtil.isNullOrEmpty(accountsEntitiesForMemberAndAdmin))
			return null;
		accountsEntitiesForMemberAndAdmin.remove(null);
		log.info("findAllByAppUserId : method call completed");
		return AccountsMapper.mapper.toJsons(accountsEntitiesForMemberAndAdmin);
	}
AccountsMapperImpl:
----------------------------

    public Set<Accounts> toJsons(Set<AccountsEntity> accountsEntities) {
        if ( accountsEntities == null ) {
            return null;
        }

        Set<Accounts> set = new LinkedHashSet<Accounts>( Math.max( (int) ( accountsEntities.size() / .75f ) + 1, 16 ) );
        for ( AccountsEntity accountsEntity : accountsEntities ) {
            set.add( toJson( accountsEntity ) );
        }

        return set;
    }
	
	 public Accounts toJson(AccountsEntity accountsEntity) {
        if ( accountsEntity == null ) {
            return null;
        }

        Accounts accounts = new Accounts();

        Long appUserId = accountsEntityAccountOwnerAppUserId( accountsEntity );
        if ( appUserId != null ) {
            accounts.setAccountOwnerId( String.valueOf( appUserId ) );
        }
        accounts.setAccountName( accountsEntity.getAccountName() );
        accounts.setAccountType( accountsEntity.getAccountType() );
        accounts.setAccountImage( accountsEntity.getAccountImage() );
        if ( accountsEntity.getCreatedBy() != null ) {
            accounts.setCreatedBy( String.valueOf( accountsEntity.getCreatedBy() ) );
        }
        accounts.setCreationDateTime( TimeMapper.updateTimeFormat( accountsEntity.getCreationDateTime() ) );
        accounts.setUpdationDateTime( TimeMapper.updateTimeFormat( accountsEntity.getUpdationDateTime() ) );
        if ( accountsEntity.getAccountId() != null ) {
            accounts.setAccountId( String.valueOf( accountsEntity.getAccountId() ) );
        }
        accounts.setVersion( accountsEntity.getVersion() );
        accounts.setActive( accountsEntity.isActive() );

        bindMembers( accountsEntity, accounts );

        return accounts;
    }
	
	@AfterMapping
	@Named("AccountMembers")
	default void bindMembers(AccountsEntity accountsEntity, @MappingTarget Accounts accounts) {
		List<MembersEntity> members = accountsEntity.getAppMembers();
		if (null != members) {
			members.removeIf(member -> null == member.getAccount());
			accounts.setAppMembers(RegisteredProfileToClientTransformer.transformer.registeredEntitiesToClientProfiles(
					members.stream().filter(member -> null == member.getTwilioPhoneNumberEntity())
							.map(member -> member.getRegisteredProfileEntity()).collect(Collectors.toSet())));
		}

		if (null != accountsEntity.getInvities())
			accounts.setNonAppMembers(accountsEntity.getInvities().stream()
					.filter(invite -> null == invite.getTwilioPhoneNumberEntity()).map(invite -> invite.getNonAppUser())
					.map(NonAppUserMapper.mapper::toJson).collect(Collectors.toSet()));
	}
NonAppUserMapper:
---------------------------

import java.util.Set;

import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

import co.cfly.fp.profile.registeredprofile.RegisteredProfileMapper;
import co.cfly.fp.util.mapper.IgnoreUnmappedMapperConfig;
import co.cfly.fp.util.utitlity.TimeMapper;

@Mapper(uses = { TimeMapper.class , RegisteredProfileMapper.class }, config = IgnoreUnmappedMapperConfig.class)
public interface NonAppUserMapper {

	NonAppUserMapper mapper = Mappers.getMapper(NonAppUserMapper.class);

	@Mapping(source = "firstName", target = "name")
	@Mapping(source = "email", target = "mailId")
	NonAppUser toJson(NonAppUserEntity entity);
	
	Set<NonAppUser> toJsons(Set<NonAppUserEntity> entity);
	
	
	@InheritInverseConfiguration
	NonAppUserEntity toEntity(NonAppUser json);
}
AccountsMapper:
----------------------

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.mapstruct.AfterMapping;
import org.mapstruct.BeanMapping;
import org.mapstruct.IterableMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;

import co.cfly.fp.nonapp.NonAppUserMapper;
import co.cfly.fp.profile.common.MembersEntity;
import co.cfly.fp.profile.registeredprofile.RegisteredProfileMapper;
import co.cfly.fp.profile.transformer.RegisteredProfileToClientTransformer;
import co.cfly.fp.util.mapper.IgnoreUnmappedMapperConfig;
import co.cfly.fp.util.utitlity.TimeMapper;

@Mapper(uses = { TimeMapper.class, RegisteredProfileMapper.class },config = IgnoreUnmappedMapperConfig.class)
public interface AccountsMapper {

	public AccountsMapper mapper = Mappers.getMapper(AccountsMapper.class);

	@Mapping(source = "accountsEntity.accountOwner.appUserId", target = "accountOwnerId")
	@Mapping(target = "appMembers", ignore = true) // Ignoring appMembers and NonAppMembers to avoid incorrect values
	@Mapping(target = "nonAppMembers", ignore = true)
	@BeanMapping(qualifiedByName = "AccountMembers")
	Accounts toJson(AccountsEntity accountsEntity);

	Set<Accounts> toJsons(Set<AccountsEntity> accountsEntities);

	@AfterMapping
	@Named("AccountMembers")
	default void bindMembers(AccountsEntity accountsEntity, @MappingTarget Accounts accounts) {
		List<MembersEntity> members = accountsEntity.getAppMembers();
		if (null != members) {
			members.removeIf(member -> null == member.getAccount());
			accounts.setAppMembers(RegisteredProfileToClientTransformer.transformer.registeredEntitiesToClientProfiles(
					members.stream().filter(member -> null == member.getTwilioPhoneNumberEntity())
							.map(member -> member.getRegisteredProfileEntity()).collect(Collectors.toSet())));
		}

		if (null != accountsEntity.getInvities())
			accounts.setNonAppMembers(accountsEntity.getInvities().stream()
					.filter(invite -> null == invite.getTwilioPhoneNumberEntity()).map(invite -> invite.getNonAppUser())
					.map(NonAppUserMapper.mapper::toJson).collect(Collectors.toSet()));
	}
	
	@Mapping(source = "accountsEntity.accountOwner.appUserId", target = "accountOwnerId")
	@Mapping(target = "appMembers", ignore = true) // Ignoring appMembers and NonAppMembers to avoid incorrect values
	@Mapping(target = "nonAppMembers", ignore = true)
	@Named(value = "AccountEntityToJson")
	Accounts entityToJsonWithoutMember(AccountsEntity accountsEntity);

	@IterableMapping(qualifiedByName = "AccountEntityToJson")
	Set<Accounts> entityToJsonWithoutMembers(Set<AccountsEntity> accountsEntities);

}

Note: Code breaks at,
if (null != accountsEntity.getInvities())
accounts.setNonAppMembers(accountsEntity.getInvities().stream()
.filter(invite → null == invite.getTwilioPhoneNumberEntity()).map(invite → invite.getNonAppUser())
.map(NonAppUserMapper.mapper::toJson).collect(Collectors.toSet()));

Has anyone encountered a similar issue or have any insights into what might be causing this discrepancy? Any help would be greatly appreciated! Thank you.

There are many possible reasons for a problem in your code. Try to create an isolated test case(https://github.com/hibernate/hibernate-test-case-templates/blob/master/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/JPAUnitTestCase.java) that reproduces the issue, then we can take a look into this. With all the Spring Data stuff going on, we can’t really figure out what the problem is.