2

How can I make sure if the object already exist its not created again.

@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;
    }
}

I did this way but as it is still keep creating new db entries instead of returning the new one.

digitalized
  • 59
  • 1
  • 10
  • Entities are distinguished by their primary keys. Only if the product name (or description, or both together) were the PK would your approach avoid creating duplicates. – John Bollinger Nov 13 '17 at 21:59
  • Additionally, if the PK were the item name only then your approach would clobber the existing product description for products that were already in the DB. – John Bollinger Nov 13 '17 at 22:01

2 Answers2

6

While John's answer will work in most cases there is a multi threading issue, that might cause one call to fail, if two threads invoke getOrCreateProduct simultaneously. Both threads might try to find an existing product and enter the NoResultException block, if there is none. Then both will create a new product and try to merge it. On transaction.commit() only one will succeed and the other thread will enter the PersistenceException block.

This can be either handled by synchronizing your method (with impacts on performance) optionally with double checked locking or as you're already using spring, you could use spring's @Retryable feature.

Here are examples for the different ways. All of the methods will be thread safe and working. But performance wise the getOrCreateProductWithSynchronization will be worst as it synchronizes every call. The getOrCreateProductWithDoubleCheckedLocking and getOrCreateProductWithRetryable should almost be the same from a performance perspective. That is you have to decide whether to go with the additional code complexity introduced by the double-checked-locking or the usage of the spring-only @Retryable feature.

@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
}

UPDATE: One more thing to note. The implementations using synchronized will only work correctly, if you got only one instance of your service. That is in a distributed setup these methods still might fail, if invoked in parallel on two ore more instances of your service. The @Retryable solution will handle this correctly as well and thus should be the preferred solution.

dpr
  • 10,591
  • 3
  • 41
  • 71
0

Since your approach should work if the product name, the product description, or the two together were the primary key for Product entities, I conclude that the PK is something else -- probably a surrogate key, since that's what JPA is tooled to use by default. If you want to use the product name to decide whether to create a new persistent entity or use an existing one then you need to perform a search on the product name. Something like this, for example:

    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;
    }

Note well that that particular implementation returns a "managed" entity if it returns one at all. This may or may not be what you want. If you want a detached one instead, then you can certainly detach it manually before returning (but in that case, don't forget to flush it first if it is new).

You might also want to support this by setting a uniqueness constraint on the product name.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • I like your approach a lot i was thinking about something simular. And thanks for the updates on the query too. Sadly when I do spring tells me not allowed to create transactions on shared entity manager, and if I anotate the method/class as @transactional it says the applicationContext.xml cant find the classpath. Any ides for that? – digitalized Nov 13 '17 at 23:40
  • I figured out. I removed the "@PersistenceContext" and "@Autowired" the EntityManagerFactory and created a new entitymanager in the method instead and worked like a charm. thanks for your help. – digitalized Nov 13 '17 at 23:55
  • 1
    There is an issue with this solution as it doesn't account for multi threading. I.e. two threads enter the `NoResultException` block and both try to merge the new entity before the other thread commits it's changes. One thread will succeed and the other one will find itself in the `PersistenceException` block. I posted an answer that should handle these situations correctly. – dpr Nov 14 '17 at 09:47
  • In practice, @dpr, since the OP is operating (as he did not initially disclose) within the regime of a more broadly scoped transaction manager, there's not much to be done about that case here. The transaction commit will happen later, under container control. It may indeed fail, and the application does indeed need to be prepared for that. – John Bollinger Nov 14 '17 at 13:58
  • I strongly recommend the @Retryable answer below, as this will only work if your app is a singleton process. That's usually not the case for high availability. – Joshua Davis Nov 03 '18 at 10:52