当前位置: 首页 > 知识库问答 >
问题:

DataIntegrityViolationException:具有相同标识符值的不同对象已与会话关联

赵佐
2023-03-14

此方法以事务开头

@Transactional(propagation = Propagation.REQUIRED)
public Account generateDirectCustomerAccountEntity(AccountReq source, Map<String, String> headers, String workflowId) {
    Account dbAccount = dCustomerModelToEntityMapper.map(source, headers);
    dbAccount = saveAccount(source, dbAccount, headers, workflowId);
    return dbAccount;
}   

这是一个映射器类,我在其中创建DB帐户实体并映射地址和联系人。

@Override
@LogExecutionTime(log=true)
public Account map(AccountReq accountReq, Map<String, String> headers) {
        logger.info("AccountModelToEntityMapper.mapDirectCustomer*****START");
        Account dbAccount = new Account();
        AccountCode accountCode = dbService.getAccountCodeForDirectCustomer(accountReq);
        String accountId = dbService.genarateAccountId(accountCode);
        logger.info("genarated AccountID for Direct Customer is={}", accountId);
        dbAccount.setAccountId(accountId);
        setAccountContact(dbAccount, accountReq, headers);
        setAddress(dbAccount, accountReq);
        dbAccount.setAccountType(accountCode.getCodeId());
        return dbAccount;
}

当我不在地图内调用下面的方法时,一切都正常工作。但是当我调用它时,我得到了标题中描述的错误。

private void setAccountContact(Account dbAccount, AccountReq accountReq, Map<String, String> headers) {

    try {
        if (null != accountReq.getContacts() && !accountReq.getContacts().isEmpty()) {
            logger.info("setAccountContact accountReq.getContacts().size()={}", accountReq.getContacts().size());
            List<String> emailList=new ArrayList<>();
            for (ContactReq contact : accountReq.getContacts()) {
                String email = contact.getEmail();
                logger.info("setAccountContact:"+email);
                if(emailList.contains(email)) {
                    logger.error("ERROR={}", "same emailId sent in multiple contacts");
                    throw new AccountException(AccountConstant.INVALID_FIELDS, AccountConstant.INVALID_FIELDS_CODE);
                
                }
                emailList.add(email);
                Contact dbcontact = contactRepository.findByEmail(email);
                if (null == dbcontact) {
                    dbcontact = new Contact();
                    dbcontact.setCreatedAt(dbAccount.getCreatedAt());
                    dbcontact.setCreatedBy(dbAccount.getCreatedBy());
                    dbcontact.setEmail(contact.getEmail());
                    dbcontact.setFirstName((contact.getFirstName()));
                    dbcontact.setLastName((contact.getLastName()));
                    dbcontact.setPhone(contact.getPhoneNumber());
                    dbcontact.setStatus(AccountConstant.STATUS_ACTIVE);
                    logger.info("contactRepository={} {}", contactRepository,contact.getEmail());

                    try {
                        dbcontact = contactRepository.save(dbcontact);
                    } catch (Exception e) {

                        logger.error("ERROR in updating contact table={}", e);
                        throw new InternalServerException(AccountConstant.INTERNAL_SERVER_ERROR,
                                AccountConstant.INTERNAL_SERVER_ERROR_CODE);
                    }
                }


                AccountContact dbAccountContact = new AccountContact();
                dbAccountContact.setCreatedAt(dbAccount.getCreatedAt());
                dbAccountContact.setCreatedBy(dbAccount.getCreatedBy());
                dbAccountContact.setStatus(AccountConstant.STATUS_ACTIVE);
                ContactRole contactRole = getContactRole(contact.getType());
                if (null == contactRole) {
                    logger.error("ERROR={}", "contact type is invalid");
                    throw new AccountException(AccountConstant.INVALID_FIELDS, AccountConstant.INVALID_FIELDS_CODE);
                }
                dbAccountContact.setContactRole(contactRole);
                dbAccountContact.setAccount(dbAccount);
                dbAccountContact.setContact(dbcontact);
                if (null != dbcontact.getAccountContact() && !dbcontact.getAccountContact().isEmpty()) {

                    logger.error("dbcontact.getAccountContact() is not null, dbcontact.getAccountContact().size()={}",
                            dbcontact.getAccountContact().size());  

                    List<AccountContact> accountContactList = dbcontact.getAccountContact();
                    accountContactList.add(dbAccountContact);
                } else {
                    logger.error("getAccountStatusHistory is null");
                    List<AccountContact> accountContactList = new ArrayList<>();
                    accountContactList.add(dbAccountContact);
                    dbcontact.setAccountContact(accountContactList);
                }
                
            
                    if (null != contact && AccountConstant.ADMIN_CONTACT_ROLE.equalsIgnoreCase(contact.getType())) {
                        if (null != contact.getAdminId()) {
                            dbService.saveExternalID(String.valueOf(dbcontact.getContactId()), contact.getAdminId(), headers, AccountConstant.STATUS_ACTIVE,
                                    CosConstants.ID_TYPE);
                        }
                    }
                

            }
        }
    } catch (Exception e) {
        logger.error("ERROR in setAccountContact to dbAccount contacts={},{}", accountReq.getContacts(), e);
        throw e;
    }
}

@LogExecutionTime(log = true)
public Account saveDirectCustomerAccount(AccountReq source, Account dbAccount, Map<String, String> headers, String workflowId) {
    
        try {
            String statusCode=AccountConstant.INITIAL_STATUS_CODE;
            dbAccount = updateAccountStatusHistory(dbAccount, statusCode);

        } catch (Exception e) {

            logger.error("ERROR in updateAccountStatusHistory={}", e);
            throw new InternalServerException(AccountConstant.INTERNAL_SERVER_ERROR,
                    AccountConstant.INTERNAL_SERVER_ERROR_CODE);
        }

        return dbAccount;
    }

@LogExecutionTime(log = true)
private Account updateAccountStatusHistory(Account dbAccount, String statusCode) {

    logger.info("updateAccountStatusHistory *****START");
    AccountStatusHistory accountStatusHistory = new AccountStatusHistory();
    accountStatusHistory.setAccount(dbAccount);
    accountStatusHistory.setCreatedAt(dbAccount.getCreatedAt());
    accountStatusHistory.setCreatedBy(dbAccount.getCreatedBy());
    try {
        updateAccountStatus(dbAccount, statusCode, accountStatusHistory);
    } catch (Exception e) {

        logger.error("ERROR updateAccountStatus e={}", e);
    }

    if (null != dbAccount.getAccountStatusHistory()) {

        logger.error("getAccountStatusHistory not null");
        dbAccount.getAccountStatusHistory().add(accountStatusHistory);

    } else {
        logger.error("getAccountStatusHistory is null");
        List<AccountStatusHistory> accountStatusHistoryList = new ArrayList<>();
        accountStatusHistoryList.add(accountStatusHistory);
        dbAccount.setAccountStatusHistory(accountStatusHistoryList);
    }

    Account createdAccount = saveAccount(dbAccount);
    logger.debug("createdAccount.getAccountId()={}", createdAccount.getAccountId());
    return createdAccount;

}

最后的方法是

public Account saveAccount(Account dbAccount) {
    try {

        Account createdAccount = accountRepository.save(dbAccount);
        logger.debug("createdAccount.getAccountId()={}", createdAccount.getAccountId());
        return createdAccount;
    } catch (Exception e) {

        logger.error("ERROR in account creation={}", e);
        throw new InternalServerException(AccountConstant.INTERNAL_SERVER_ERROR,
                AccountConstant.INTERNAL_SERVER_ERROR_CODE);
    }
}

异常堆栈跟踪

null

共有1个答案

夏宪
2023-03-14

好吧,答案会有点复杂,但请耐心听我说。

首先,根本原因是手动生成的ID

SetAccountContact(...)内部,您有:DbAccountContact.SetAccount(dbAccount),您在其中分配以前在外部映射(...)方法中创建的帐户。让我们将此帐户对象称为A

稍后,在saveAccount(...)中,您可以调用AccountRepository.save(dbAccount)。此调用返回一个account对象,我们将其称为b

现在,Spring Data JPA存储库save方法决定在save内部调用EntityManager.merge()还是调用EntityManager.persist(),这取决于它是否认为实体是新的。默认情况下,它假定任何已经有id的实体都不能是新实体(因此选择merge())。

将非托管/分离的实体传递给EntityManager.merge()时,总是返回输入实体的副本。这意味着A!=B

这正是具有相同标识符值的不同对象已经与session错误相关联的地方。在事务提交时,具有相同IDaccountContact的不同副本(分配给DBAccountContactA和刚从save返回的B)。这样的状态在JPA中是被禁止的,因为JPA不知道对象的哪个版本‘获胜’。请注意,如果save调用了EntityManager.persist(),则不会创建任何副本,原始的帐户将变成托管帐户,因此问题不会存在。

解决此问题的方法:

  1. 以Hibernate的序列生成器形式封装底层数据库的ID生成功能

您使用的是dbservice.genarateAccountId(accountCode),所以我假设涉及一些自定义逻辑,而简单的@generatedvalue(strategy=SEQUENCEAUTOTABLE)是不行的。然而,这些并不是唯一的选择。请参阅这篇关于如何推出自定义id生成器的文章。

(注意:本文建议从SequenceStyleGenerator继承,它允许您将DB序列与额外内容连接起来。但是,如果您实现更通用的IdentifierGenerator,您将获得对SessionImplementor的访问权,您可以使用它在DB上调用任意SQL,而不一定访问序列)。

我个人强烈推荐这个选项。但是,如果这对你不起作用,那么:

您可以实现persistable和实现isnew()来告诉spring数据‘嘿,不要被id骗了,这个对象实际上是一个新对象!’。正如您已经注意到的,这样做解决了问题。当然,需要注意的是account的任何实例,无论是新的还是旧的,现在都将被jparepository.save()视为新的。如果您在应用程序的其他地方合并分离的account实体,这将会产生问题(如果没有,您应该完全没有问题)。

另一种解决方案是在Map中创建新帐户后立即调用AccountRepository.save():

Account dbAccount = new Account(); //Account A created
dbService.genarateAccountId(accountCode);
dbAccount = accountRepository.save(dbAccount); //Account A passed to `save`, `save` returns Account B, Account A is not referenced anywhere anymore, problem solved

(您还可以稍后调用save,但必须在出现问题的dbAccountContact.setAccount(dbAccount)行之前调用)。

这避免了持久性上下文中存在两个account副本的问题。但是,在执行查询时可能会遇到问题,例如:

Contact dbcontact = contactRepository.findByEmail(email);

在到达这一行之前,您需要确保account的所有必需关联都被正确填充,因为在这一点上,持久性上下文将被刷新。如果不能满足此要求,则可以将flushmode.commit与查询方法本身一起使用:

@QueryHints(value = { @QueryHint(name = org.hibernate.annotations.QueryHints.FLUSH_MODE, value = "COMMIT") }) //  this prevents a flush before querying
Contact findByEmail(String email);

(不确定上述问题是否适用于您,但如果适用,只需重新排列代码,以便在执行任何查询之前保存account的当前状态不会触发约束冲突。或者,在saveaccount之前考虑调用query方法)。此外,在此场景中,不必担心将persist操作级联。在事务提交时仍应发生级联。

当然,这引入了一个泄漏的抽象,因此您可能不应该认真考虑这个解决方案。我只是说会管用的。

 类似资料:
  • 我有以下问题: 有人对此有解释吗?我考虑从我的业务服务中的hibernate会话中重新加载对象,并从Struts HTTP会话中复制实体对象中的数据。 Hibernate映射

  • null 对于我的数据,我有时会遇到这样的问题:A有一组不同的B对象,而这些B对象引用同一个C对象。 当我调用时,我会得到一个hibernate错误:。我知道hibernate不能在同一个会话中插入/更新/删除同一个对象两次,但是有什么方法可以解决这个问题吗?这似乎并不是一种罕见的情况。 在我研究这个问题的过程中,我看到有人建议使用,但当我这样做时,任何“冲突”对象都会作为所有值都设置为null的

  • 我们试图通过Keycloak验证现有用户,因此实现了自定义SPI,并将自定义SPI添加为用户联合 原因:javax.persistence.EntityExistsException:具有相同标识符值的不同对象已与会话关联:[org.keycloak.storage.jpa.entity.FederatedUserRoleMappingEntity#org.keycloak.storage.jpa

  • 问题内容: 在使用Spring和Hibernate的应用程序中,我解析CSV文件,并在每次从CSV文件中读取记录时通过调用来填充db 。 我的域模型: “家庭”有很多“子家庭” “子家庭”有很多“位置” “ Locus”属于“ Species” 都是双向映射。 码: 使用以下方法将物种分配给场所,该方法仅访问DAO层: Hibernate给出以下错误: 有小费吗? 问题答案: 使用。该异常表示当前

  • 问题内容: 我有两个用户对象,而在尝试使用以下方法保存对象时 我收到以下错误: 我正在使用创建会话 我还尝试过在保存之前进行操作,但仍然没有运气。 这是我第一次在用户请求到来时获取会话对象,因此我要为什么要说该对象存在于会话中。 有什么建议么? 问题答案: 我已经多次发生此错误,很难追踪… 基本上,hibernate是指您有两个具有相同标识符(相同主键)但不是相同对象的对象。 我建议您分解代码,即

  • 问题内容: 我基本上在此配置中有一些对象(实际数据模型要复杂一些): A与B有多对多关系。(B具有) B与C具有多对一关系(我已设置为) C是一种类型/类别表。 另外,我可能应该提到主键是在保存时由数据库生成的。 使用我的数据,有时我会遇到一个问题,其中A具有一组不同的B对象,而这些B对象引用了相同的C对象。 打电话时,我收到了一个hibernate错误消息:。我知道hibernate无法在同一会