【问题标题】:Entity manager get or create实体管理器获取或创建
【发布时间】:2018-04-26 17:06:55
【问题描述】:

我如何确定对象是否已经存在而不是再次创建。

@Component
public class ProductServiceImpl implements ProductService
{
    @PersistenceContext
    private EntityManager em;

    public Product getOrCreateProduct(String productName, String peoductDescr)
    {
        Product product =(new Product (productName, peoductDescr));
        em.merge(product);
        return product;
    }
}

我是这样做的,但它仍然会继续创建新的数据库条目,而不是返回新的条目。

【问题讨论】:

  • 实体通过它们的主键来区分。只有当产品名称(或描述,或两者一起)是 PK 时,您的方法才能避免创建重复项。
  • 此外,如果 PK 只是项目名称,那么您的方法将破坏数据库中已有产品的现有产品描述。

标签: java spring hibernate-entitymanager


【解决方案1】:

虽然 John 的答案在大多数情况下都有效,但存在多线程问题,如果两个线程同时调用 getOrCreateProduct,这可能会导致一次调用失败。如果没有,两个线程都可能会尝试查找现有产品并输入NoResultException 块。然后两者都将创建一个新产品并尝试合并它。在transaction.commit() 上,只有一个线程会成功,另一个线程将进入PersistenceException 块。

这可以通过选择使用双重检查锁定来同步您的方法(对性能有影响)来处理,或者由于您已经在使用 spring,您可以使用 spring 的 @Retryable 功能。

以下是不同方式的示例。所有方法都将是线程安全的并且可以工作。但是在性能方面getOrCreateProductWithSynchronization 将是最差的,因为它会同步每个调用。从性能的角度来看,getOrCreateProductWithDoubleCheckedLockinggetOrCreateProductWithRetryable 应该几乎相同。也就是说,您必须决定是使用双重检查锁定引入的额外代码复杂性还是使用纯弹簧 @Retryable 功能。

@Transactional(propagation = Propagation.REQUIRES_NEW)
public synchronized Product getOrCreateProductWithSynchronization(final String productName, final String productDescr) {

  Product product = findProduct(productName);
  if (product != null) {
    return product;
  }

  product = new Product(productName, productDescr);
  em.persist(product);

  return product;
}


@Transactional(propagation = Propagation.REQUIRES_NEW)
public Product getOrCreateProductWithDoubleCheckedLocking(final String productName, final String productDescr) {

  Product product = findProduct(productName);
  if (product != null) {
    return product;
  }

  synchronized (this) {
    product = findProduct(productName);
    if (product != null) {
      return product;
    }

    product = new Product(productName, productDescr);
    em.persist(product);
  }

  return product;
}


@Retryable(include = DataIntegrityViolationException.class, maxAttempts = 2)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Product getOrCreateProductWithRetryable(final String productName, final String productDescr) {

  Product product = findProduct(productName);
  if (product != null) {
    return product;
  }

  product = new Product(productName, productDescr);
  em.persist(product);
  return product;
}


private Product findProduct(final String productName) {
  // try to find an existing product by name or return null
}

更新: 还有一点需要注意。使用synchronized 的实现只有在您只有一个服务实例时才能正常工作。也就是说,在分布式设置中,如果在您的服务的两个或更多实例上并行调用这些方法,这些方法仍然可能会失败。 @Retryable 解决方案也将正确处理此问题,因此应该是首选解决方案。

【讨论】:

  • 100% 同意@Retryable 答案是首选解决方案。大多数应用的 SLA 需要多个节点(甚至是一个集群)。
  • getOrCreateProductWithRetryable 正是我所需要的。谢谢!
【解决方案2】:

由于如果产品名称、产品描述或两者一起是Product 实体的主键,您的方法应该有效,因此我得出结论认为 PK 是其他东西——可能是代理键,因为这就是 JPA被工具默认使用。如果要使用产品名称来决定是创建新的持久实体还是使用现有实体,则需要对产品名称执行搜索。像这样的东西,例如:

    public Product getOrCreateProduct(String productName, String productDescr) {
        Product product;

        try {
            TypedQuery<Product> productForName = em.createQuery(
                    "select p from Product p where p.name = ?1", Product.class);
            EntityTransaction transaction;

            productForName.setParameter(1, productName);

            /*
             * The query and any persist() operation required should be
             * performed in the same transaction.  You might, however, want
             * to be a little more accommodating than this of any transaction
             * that is already in progress.
             */
            transaction = em.getTransaction();
            transaction.begin();
            try {
                product = productForName.getSingleResult();
            } catch (NoResultException nre) {
                product = new Product(productName, productDescr);
                em.persist(product);
            }
            transaction.commit();
        } catch (PersistenceException pe) {
            // ... handle error ...
        }

        return product;
    }

请注意,如果该特定实现返回一个“托管”实体,那么它会返回一个。这可能是也可能不是您想要的。如果您想要一个分离的,那么您当然可以在返回之前手动分离它(但在这种情况下,如果它是新的,请不要忘记先冲洗它)。

您可能还希望通过对产品名称设置唯一性约束来支持这一点。

【讨论】:

  • 我非常喜欢你的方法,我正在考虑一些类似的事情。并感谢查询的更新。可悲的是,当我做 spring 告诉我不允许在共享实体管理器上创建事务时,如果我将方法/类注释为 @transactional 它说 applicationContext.xml 找不到类路径。有什么想法吗?
  • 我想通了。我删除了 EntityManagerFactory 的“@PersistenceContext”和“@Autowired”,并在该方法中创建了一个新的实体管理器,并像一个魅力一样工作。感谢您的帮助。
  • 此解决方案存在问题,因为它不考虑多线程。 IE。两个线程进入NoResultException 块并且都尝试在另一个线程提交它的更改之前合并新实体。一个线程将成功,而另一个线程将发现自己位于PersistenceException 块中。我发布了一个应该正确处理这些情况的答案。
  • 在实践中,@dpr,由于 OP 在范围更广的事务管理器的制度下运行(正如他最初没有透露的那样),因此这里没有太多关于这种情况的事情要做。事务提交将在容器控制下稍后发生。它确实可能会失败,应用程序确实需要为此做好准备。
  • 我强烈推荐下面的@Retryable 答案,因为这仅在您的应用程序是单例进程时才有效。高可用性通常不是这种情况。
猜你喜欢
  • 2014-09-13
  • 1970-01-01
  • 2019-11-23
  • 2014-07-22
  • 1970-01-01
  • 1970-01-01
  • 2013-04-14
  • 2011-07-08
  • 2016-02-25
相关资源
最近更新 更多