【问题标题】:JPA: locking when performing an indirect insertionJPA:执行间接插入时锁定
【发布时间】:2016-12-04 12:44:24
【问题描述】:

我正在编写一个跟踪药物使用情况的软件。我正在使用 JPA 与数据库进行交互。我的模型由两个实体组成:PrescriptionDose。每个Prescription 都有一个Dose 实例的集合,这些实例代表作为此处方的一部分给予患者的剂量,如下所示:

Prescription.java

@Entity
@XmlRootElement
public class Prescription {

    private long id;
    private Collection<Dose> doses = new ArrayList<Dose>();
    /**
     * Versioning field used by JPA to track concurrent changes.
     */
    private long version;
    // Other properties omitted...

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    // We specify cascade such that when we modify this collection, it will propagate to the DOSE table (e.g. when
    // adding a new dose to this collection, a corresponding record will be created in the DOSE table).
    @OneToMany(mappedBy = "prescription", cascade = CascadeType.ALL)
    public Collection<Dose> getDoses() {
        // todo update to list or collection interface.
        return doses;
    }

    public void setDoses(Collection<Dose> doses) {
        this.doses = doses;
    }

    @Version
    public long getVersion() {
        return version;
    }

    /**
     * Application code should not call this method. However, it must be present for JPA to function.
     * @param version
     */
    public void setVersion(long version) {
        this.version = version;
    }
}

Dose.java

@Entity
@XmlRootElement
public class Dose {

    private long id;
    private Prescription prescription;
    // Other properties omitted...

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    @XmlTransient
    @ManyToOne
    @JoinColumn(name = "PRESCRIPTION_ID") // Specifies name of column pointing back to the parent prescription.
    public Prescription getPrescription() {
        return prescription;
    }

    public void setPrescription(Prescription prescription) {
        this.prescription = prescription;
    }

}

Dose 只能存在于Prescription 的上下文中,因此Dose 通过将其添加到其处方的剂量集合中间接插入到数据库中:

DoseService.java

@Stateless
public class DoseService {

    @PersistenceContext(unitName = "PrescriptionUnit")
    private EntityManager entityMgr;

    /**
     * Insert a new dose for a given prescription ID.
     * @param prescriptionId The prescription ID.
     * @return The inserted {@code Dose} instance if insertion was successful,
     * or {@code null} if insertion failed (if there is currently no doses available for the given prescription ID).
     */
    @TransactionAttribute(value = TransactionAttributeType.REQUIRED)
    public Dose addDose(long prescriptionId) {
        // Find the prescription.
        Prescription p = entityMgr.find(Prescription.class, prescriptionId);
        if (p == null) {
            // Invalid prescription ID.
            throw new IllegalArgumentException("Prescription with id " + prescriptionId + " does not exist.");
        }
        // TODO is this sufficient locking?
        entityMgr.lock(p, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
        Dose d = null;
        if (isDoseAvailable(p)) {
            // A dose is available, create it and insert it into the database.
            d = new Dose();
            // Setup the link between the new dose and its parent prescription.
            d.setPrescription(p);
            p.getDoses().add(d);
        }
        try {
            // Flush changes to database.
            entityMgr.flush();
            return d;
        } catch (OptimisticLockException ole) {
            // Rethrow application-managed exception to ensure that caller will have a chance of detecting failure due to concurrent updates.
            // (OptimisticLockExceptions can be swallowed by the container)
            // See "Recovering from Optimistic Failures" (page 365) in "Pro JPA 2" by M. Keith and M. Schincariol for details.
            throw new ChangeCollisionException();
        }
    }


    /**
     * Checks if a dose is available for a given prescription.
     * @param p The prescription for which to look up if a dose is available.
     * @return {@code true} if a dose is available, {@code false} otherwise.
     */
    @TransactionAttribute(value = TransactionAttributeType.MANDATORY)
    private boolean isDoseAvailable(Prescription p) {
        // Business logic that inspects p.getDoses() and decides if it is safe to give the patient a dose at this time.
    }

}

addDose(long) 可以同时调用。在决定剂量是否可用时,业务逻辑会检查处方的剂量集合。如果这个集合被同时修改(例如通过并发调用addDose(long)),事务应该失败。我使用LockModeType.OPTIMISTIC_FORCE_INCREMENT 来实现这一点(而不是在 DOSE 表上获取表锁)。在Pro JPA 2 by Keith and Schincariol 我读到:

写锁保证了乐观读锁所做的一切,但是 还承诺增加交易中的版本字段 无论用户是否更新了实体。 [...] 这 使用 OPTIMISTIC_FORCE_INCREMENT 的常见情况是保证 实体关系变化的一致性(通常是 在对象中时与目标外键的一对多关系) model 实体关系指针发生变化,但在数据模型中 实体表中的列没有变化。

我对这种锁定模式的理解正确吗?如果处方的剂量集合发生任何变化(无论是添加、删除或更新集合中的任何剂量),我当前的策略是否确保addDose 事务将失败?

【问题讨论】:

  • 据我了解,如果对象上存在并发事务,则无论该并发事务是否修改您的对象,您的事务都会失败。在乐观并发中,如果修改对象的版本和当前对象的版本不匹配,事务就会失败。
  • 您使用的是什么容器/Web 应用程序服务器?并非所有容器都以相同的方式实现 JPA 或 EJB 标准。有些完全忽略某些参数或注释。
  • @JeffreyColeman 我正在使用 GlassFish 开源版本。
  • @JanusVarmarken 感谢您的回复。那么在那种情况下,我会说我的第一个猜测可能是错误的,但是当我有机会时,我会为您检查文档。 GlassFish 通常实现所有内容,因为它是 JEE 规范的试验场。

标签: java jpa


【解决方案1】:

看起来不错。

但是,我建议先对其进行测试...更简单的方法是通过调试...使用您喜欢的 IDE,在句子后面设置一个调试点:

entityMgr.lock(p, LockModeType.OPTIMISTIC_FORCE_INCREMENT);

稍后,尝试从两个不同的客户端调用您的addDose(prescriptionId),提供相同的处方ID ...并让一个客户端先完成,看看另一个客户端会发生什么。

【讨论】:

  • 感谢您的调试建议。我加了一个+1。但是,我正在寻找比“看起来正确”更强大的正确性确认,以便接受答案(例如,解释特定于这种情况的锁定细节的答案)。
【解决方案2】:

This answer 帮助我更好地理解了OPTIMISTIC_WRITE_LOCK 并让我确信我的实现是正确的。

以下是更详细的解释(引用出现在我自己撰写的报告中):

虽然 EJB 事务可能有助于防止对实体的持久状态进行并发更改,但在这种情况下它们是不够的。 这是因为他们无法检测到 Prescription 实体作为其对应数据库的更改 向其中添加新的Dose 时不会更改行。这源于 Dose 是关系的拥有方这一事实 在它自己和它的Prescription之间。在数据库中,该行 表示Dose 将有一个外键指向 Prescription,但代表 Prescription 的行将 没有指向任何Doses 的指针。该问题通过以下方式解决 用乐观的写锁保护Prescription 当插入新的Dose 时更新Prescription 的行(具体而言:其版本字段)。

【讨论】:

    猜你喜欢
    • 2018-03-29
    • 2018-07-01
    • 2018-02-13
    • 1970-01-01
    • 1970-01-01
    • 2016-03-06
    • 2010-10-22
    • 2023-04-05
    • 1970-01-01
    相关资源
    最近更新 更多