使用 JPA 创建唯一并合并数据库中的现有行

Posted

技术标签:

【中文标题】使用 JPA 创建唯一并合并数据库中的现有行【英文标题】:Creating unique and merging existing row in database with JPA 【发布时间】:2016-04-20 22:20:58 【问题描述】:

我有 2 张桌子:UserLoanUser 有 3 个字段:id (PK)、first_namelast_nameLoan 表有字段 user_idUser 表的外键:

通过坚持一个新的Loan,如果他的first_namelast_name 是唯一的,我需要创建新的User,否则将他的id 放入uder_id

我的贷款类源码:

 public class Loan 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private Long sum;
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "user_id")
    private User user;

... methods ...

我正在使用这种方法来持久化新的User

@PersistenceContext
private EntityManager em; 

public void save(User user) 
    if (user.getId() == null) 
         em.persist(user);
     else 
         em.merge(user);
    

当我尝试保存一个新的Loan 时,它总是会保留一个新的User,具有相同的first_namelast_name,但不同的id

 loan.setSum(sum);
 loan.setUser(new User(firstName, lastName));
 loanService.save(loan);

要使用用户的first_namelast_name 作为PK 不是解决方案,我需要id

UPDATE_1

我试图通过他的名字找到User

public User findByName(String firstName, String lastName) 
        TypedQuery<User> query = em.createQuery(
                "SELECT u FROM User u WHERE u.firstName = :firstName " +
                        "AND u.lastName = :lastName", User.class)
                .setParameter("firstName", firstName).setParameter("lastName", lastName);
        return query.getSingleResult();
    

但是当我输入新用户时出现异常:

javax.faces.FacesException: #loanBean.requestLoan(): javax.persistence.NoResultException: getSingleResult() did not retrieve any entities.

当我输入现有用户时,它会添加一个具有相同 firstNamelastName 但新 id 的新用户。 当我重复这个操作时,我得到了另一个异常:

javax.servlet.ServletException: javax.persistence.NonUniqueResultException: More than one result was returned from Query.getSingleResult()

UPDATE_2 非常感谢 Pietro Boido 提供的非常有用的建议。我在 DB 中的 first_namelast_name 字段上创建了唯一索引,并重构了 save() 方法。但是现在当我输入现有用户的数据时,我得到了新的异常

javax.servlet.ServletException: org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.6.0.v20150309-bf26070): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: java.sql.SQLIntegrityConstraintViolationException: The statement was aborted because it would have caused a duplicate key value in a unique or primary key constraint or unique index identified by 'FIRST_LAST_NAME' defined on 'USER'.
Error Code: 20000

【问题讨论】:

我不打算直接回答你的问题,但我认为这个逻辑与你的业务层完全相关,必须排除在数据访问逻辑之外。 你说得对,我更新了我的问题。 【参考方案1】:

您应该首先执行查询以按名称查找用户并仅在未找到时创建一个新用户:

User user = userService.find(firstName, lastName);
if (user == null) 
    user = loanService.createUser(new User(firstName, lastName));

loan.setSum(sum);
loan.setUser(user);
loanService.save(loan);

由于可能没有给定名称的用户,因此在查询用户时使用 getResultList,因为 getSingleResult 期望总能找到结果。

List<User> users = query.getResultList();
if (!users.isEmpty()) 
    return users.iterator().next();
 else 
    return null;

代码假定数据库在 first_name、last_name 上具有唯一索引。

您不应该对 ManyToOne 关系进行级联操作。想一想:如果你删除了一个贷款,用户也应该被删除吗?

当相关实体是相关实体的一部分并且它们的生命周期被一起管理时,应该使用级联操作。

@Entity
@Table(name="loans")
public class Loan implements Serializable 

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column(name = "total")
    private Long sum;
    @ManyToOne()
    @JoinColumn(name = "user_id")
    private User user;

    //...

这是一个可能的工作示例:

@Stateless
public class LoanService implements LoanServiceRemote 

    @PersistenceContext
    private EntityManager em;

    @Override
    public User createUser(User user) 
        em.persist(user);
        return user;
    

    @Override
    public Loan createLoan(Loan loan) 
        em.persist(loan);
        System.out.println("loan persisted: id=" + loan.getId());
        return loan;
    

    @Override
    public Loan saveLoan(Loan loan) 
        em.merge(loan);
        return loan;
    

    @Override
    public Long incrementLoan(Integer loanId, long amount) 
        Loan loan = em.find(Loan.class, loanId);
        if (loan != null) 
            long sum = loan.getSum() + amount;
            /*
             * The entity is bound to the entity manager,
             * because it was returned by the find method.
             * We can simply set its properties and
             * the entity manager will update the datasource
             * after the method returns and the transaction commits. 
             * No need to call persist or merge.
             */
            loan.setSum(sum);
            return sum;
        
        return null;
    

    @Override
    public boolean deleteLoan(Integer loanId) 
        Loan loan = em.find(Loan.class, loanId);
        if (loan != null) 
            em.remove(loan);
            return true;
        
        return false;
    

    @Override
    public Loan findLoan(Integer loanId) 
        return em.find(Loan.class, loanId);
    

    @Override
    public List<Loan> requestLoans(LoanRequest loanRequest) 
        User user;
        TypedQuery<User> query = em.createQuery("select user from User user where user.firstName = :firstName and user.lastName = :lastName", User.class);
        query.setParameter("firstName", loanRequest.getFirstName());
        query.setParameter("lastName", loanRequest.getLastName());
        List<User> users = query.getResultList();
        if (users.isEmpty()) 
            user = new User();
            user.setFirstName(loanRequest.getFirstName());
            user.setLastName(loanRequest.getLastName());
            //new entities must be persisted
            em.persist(user);
         else 
            user = users.get(0);
        

        List<Loan> loans = new ArrayList<>();
        Long[] totals = loanRequest.getTotals();
        for (int i = 0; i < totals.length; i++) 
            Loan loan = new Loan();
            loan.setSum(totals[i]);
            loan.setUser(user);
            em.persist(loan);
            loans.add(loan);
        

        return loans;
    

单元测试:

@Test
public void testLoan() 
    User user = loanService.createUser(newUser());

    Loan loan1 = new Loan();
    loan1.setSum(10L);
    loan1.setUser(user);

    Loan loan2 = loanService.createLoan(loan1);
    assertNotNull(loan2);

    Integer loanId = loan2.getId();

    assertNotNull(loanId);
    assertEquals(loan1.getSum(), loan2.getSum());
    assertEquals(loan1.getUser(), user);

    User user2 = loanService.createUser(newUser());
    loan2.setUser(user2);
    loan2.setSum(20L);
    Loan loan3 = loanService.saveLoan(loan2);
    assertLoanEquals(loan2, loan3);

    Long total = loanService.incrementLoan(loanId, 10L);
    assertNotNull(total);
    assertEquals((Long)(loan3.getSum() + 10L), total);

    loan3.setSum(total);

    Loan loan4 = loanService.findLoan(loanId);
    assertLoanEquals(loan3, loan4);

    boolean result = loanService.deleteLoan(loanId);
    assertTrue(result);

    Loan loan5 = loanService.findLoan(loanId);
    assertNull(loan5);

    Long[] totals = new Long[]1L,2L,3L;
    LoanRequest loanRequest = new LoanRequest();
    loanRequest.setFirstName("Amerigo");
    loanRequest.setLastName("Vespucci");
    loanRequest.setTotals(totals);

    List<Loan> loans = loanService.requestLoans(loanRequest);
    assertNotNull(loans);
    assertEquals(3, loans.size());
    for (int i = 0; i < 3; i++) 
        assertEquals(totals[i], loans.get(i).getSum());
        loanService.deleteLoan(loans.get(i).getId());
    




void assertLoanEquals(Loan loan1, Loan loan2) 
    assertNotNull(loan1);
    assertNotNull(loan2);
    assertEquals(loan1.getSum(), loan2.getSum());
    assertUserEquals(loan1.getUser(), loan2.getUser());
    assertEquals(loan1.getId(), loan2.getId());

void assertUserEquals(User user, User user2) 
    assertNotNull(user);
    assertNotNull(user2);
    assertEquals(user.getId(), user2.getId());
    assertEquals(user.getFirstName(), user2.getFirstName());
    assertEquals(user.getLastName(), user2.getLastName());

【讨论】:

谢谢你的建议,我试过这个并更新我的问题。 我认为问题出在此处:@ManyToOne(cascade = CascadeType.ALL) 因为当我将CascadeType 更改为MERGE 时,它一直在按我的需要工作,直到我添加了一个新的User 正如我所说的@ManyToOne(cascade = CascadeType.PERSIST, CascadeType.MERGE) 我只能创建新用户。当我尝试输入现有的时,我得到:'SQLIntegrityConstraintViolationException:该语句被中止,因为它会导致唯一或主键约束或由“客户”上定义的“FIRST_LAST_NAME”标识的唯一索引中的重复键值。错误代码:20000' @DimaSan 我建议从 ManyToOne 注释中删除级联操作。请参阅我的更新答案。 非常感谢彼得罗!

以上是关于使用 JPA 创建唯一并合并数据库中的现有行的主要内容,如果未能解决你的问题,请参考以下文章

使用 python,我如何从 csv 中获取唯一行,但获取合并了哪些行(或行中的值)的记录?

在 Postgres 中的现有列中添加“串行”

如何为 PL SQL 中的现有表创建唯一 ID?

迭代获取数据框列的最大值,加一并重复 r 中的所有行

从现有 json 列表中提取唯一值以创建唯一的列表视图构建器

合并声明行以获得唯一值