使用Spring Hibernate CrudRepository Dao模式时,如何在表上获得插入锁?

时间:2019-07-27 05:08:47

标签: spring hibernate jpa locking dao

我正在尝试在CrudRepository岛中编写一个默认方法:

  1. 锁定表以防止插入(但是可以进行读取和更新)
  2. 使用dao中的另一种方法查找现有条目。
    • 如果找到,则返回找到的条目。
    • 如果找不到,请插入提供的条目,然后返回它。
  3. 解锁表格。

我已经研究过在该方法上使用@Lock(LockModeType.PESSIMISTIC_WRITE),但是由于它没有关联的@Query注释,因此我认为它没有任何作用。

我还尝试在dao中创建锁定和解锁方法:

    @Query(value = "LOCK TABLES direct_mail WRITE", nativeQuery = true)
    void lockTableForWriting();

    @Query(value = "UNLOCK TABLES", nativeQuery = true)
    void unlockTable();

但是那些人在LOCKUNLOCK上抛出了SQLGrammerException。

我无法获得行锁,因为该行尚不存在。或者,如果确实存在,那么我将不更新任何内容,而只是不插入任何内容并继续进行其他操作。

在其他情况下,我希望使用相同的事务ID保存多条记录,因此我不能只是使该列唯一并尝试/捕获该保存。

在我的服务层中,我尝试进行一些查找并在可能的情况下进行短路,但是仍然有可能多个接近同时的调用可能导致两次尝试插入相同的数据。

由于有多个正在运行的服务实例,因此需要在数据库级别进行处理。因此,两个相互竞争的呼叫可能在与同一数据库通信的不同机器上。

这是我的存储库:

@Repository
public interface MailDao extends CrudRepository<Mail, Long> {

    default Mail safeSave(Mail mail) {
        return Optional.ofNullable(findByTransactionId(mail.getTransactionId()))
                       .orElseGet(() -> save(mail));
    }

    default DirectMail findByTransactionId(String transactionId) {
        final List<Mail> mails = findAllByTransactionId(transactionId);
        // Snipped code that selects a single entry to return
        // If one can't be found, null is returned.
    }

    @Query(value = "SELECT m " +
                   "  FROM Mail m " +
                   " WHERE m.transactionId = :transactionId ")
    List<Mail> findAllByTransactionId(@Param("transactionId") String transactionId);
}

这是Mail模型的样子:

@Entity
@Table(name = "mail")
public class Mail implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "mail_id", unique = true, nullable = false)
    private Long mailId;

    @Column(name = "transaction_id", nullable = false)
    private String transactionId;

    // Snipped code for other parameters, 
    // constructors, getters and setters.
}

这是将调用safeSave的服务方法的一般概念。

@Service
public class MailServiceImpl implements MailService {
    @Inject private final MailDao mailDao;
    // Snipped injected other stuff

    @Override
    public void saveMailInfo(final String transactionId) {
        Objects.requireNonNull(transactionId, "null transactionId passed to saveMailInfo");
        if (mailDao.findByTransactionId(transactionId) != null) {
            return;
        }
        // Use one of the injected things to do some lookup stuff
        // using external services
        if (mailDao.findByTransactionId(transactionId) != null) {
            return;
        }
        // Use another one of the injected things to do more lookup
        if (/* we've got everything we need */) {
            final Mail mail = new Mail();
            mail.setTransactionId(transactionId);
            // Snipped code to set the rest of the stuff
            mailDao.safeSave(mail);
        }
    }
}

我要防止的是对saveMailInfo的两个几乎同时的调用导致数据库中的记录重复。

基础数据库是MySQL。

1 个答案:

答案 0 :(得分:0)

我进行了一个INSERT INTO ... WHERE NOT EXISTS查询和一个自定义存储库。这在更新2的上方列出,但我也将其放在此处,以便于查找。

public interface MailDaoCustom {
    Mail safeSave(Mail mail);
}

更新了MailDao以实现它:

public interface MailDao extends CrudRepository<Mail, Long>, MailDaoCustom

然后impl看起来像这样:

public class MailDaoImpl implements MailDaoCustom {

    @Autowired private MailDao dao;
    @Autowired private EntityManager em;

    public Mail safeSave(Mail mail) {
        // Store a new mail record only if one doesn't already exist.
        Query uniqueInsert = em.createNativeQuery(
                        "INSERT INTO mail " +
                        "       (transaction_id, ...) " +
                        "SELECT :transactionId, ... " +
                        " WHERE NOT EXISTS (SELECT 1 FROM mail " +
                        "                   WHERE transaction_id = :transactionId) ");
        uniqueInsert.setParameter("transactionId", mail.getTransactionId());
        // Snipped setting of the rest of the parameters in the query
        uniqueInsert.executeUpdate();

        // Now go get the record
        Mail entry = dao.findByTransactionId(mail.getTransactionId());
        // Detach the entry so that we can attach the provided mail object later.
        em.detach(entry);

        // Copy all the data from the db entry into the one provided to this method
        mail.setMailId(entry.getMailId());
        mail.setTransactionId(entry.getTransactionId());
        // Snipped setting of the rest of the parameters in the provided mail object

        // Attach the provided object to the entity manager just like the save() method would.
        em.merge(mail);

        return mail;
    }
}