JPA: блокировка при выполнении косвенной вставки

Я пишу программный продукт, который отслеживает использование лекарств. Я использую JPA для взаимодействия с базой данных. Моя модель состоит из двух объектов:Prescription иDose, каждый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;
    }

}

A 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 Китом и Шинкариолом Я прочитал это:

Блокировка записи гарантирует все, что делает оптимистическая блокировка чтения, но также обязуется увеличивать поле версии в транзакции независимо от того, обновил ли пользователь объект или нет. [...] распространенным случаем использования OPTIMISTIC_FORCE_INCREMENT является обеспечение согласованности между изменениями отношений сущностей (часто это отношения «один ко многим» с целевыми внешними ключами), когда в объектной модели изменяются указатели отношений сущностей, но в модели данных столбцы в таблице сущностей не изменяются.

Правильно ли я понимаю этот режим блокировки? Моя текущая стратегия гарантирует, чтоaddDose транзакция потерпит неудачу, если в ЛЮБОЙ дозе рецепта произойдет ЛЮБОЕ изменение (будь то добавление, удаление или обновление какой-либо дозы в коллекции)?

 Janus Varmarken29 июл. 2016 г., 20:30
@JeffreyColeman Я использую GlassFish с открытым исходным кодом.
 Jeff Coleman29 июл. 2016 г., 20:46
@JanusVarmarken Спасибо за ответ. Ну, в таком случае я бы сказал, что мое первое предположение, вероятно, было неверным, но я проверю документацию для вас, когда у меня будет такая возможность. GlassFish, как правило, реализует все, как своего рода полигон для тестирования спецификаций JEE.
 sturcotte0629 июл. 2016 г., 20:18
Насколько я понимаю, ваша транзакция потерпит неудачу, если на объекте будет параллельная транзакция, независимо от того, изменяет ли эта параллельная транзакция ваш объект. В оптимистическом параллельном режиме транзакции завершаются неудачей, если версия измененного объекта и версия текущего объекта не совпадают.
 Jeff Coleman29 июл. 2016 г., 20:29
Какой контейнер / сервер веб-приложений вы используете? Не все контейнеры реализуют стандарты JPA или EJB одинаково. Некоторые полностью игнорируют определенные параметры или аннотации.

Ответы на вопрос(2)

Решение Вопроса

Этот ответ помог мне понятьOPTIMISTIC_WRITE_LOCK лучше и убедил меня, что моя реализация верна.

Далее следует более подробное объяснение (цитата добавлена ​​так, как она появилась в отчете, написанном мной):

Хотя транзакции EJB могут помочь предотвратить одновременные изменения в постоянном состоянии объекта, в этом случае их недостаточно. Это потому, что они не могут обнаружить изменения вPrescription сущность как соответствующая ей строка базы данных не изменяется приDose добавляется к нему. Это связано с тем, чтоDose является собственником отношений между собой и егоPrescription, В базе данных строка, которая представляетDose будет иметь внешний ключ, указывающий наPrescription, но строка, которая представляетPrescription не будет иметь указателей на любой из егоDoses. Проблема устраняется путем охраныPrescription с оптимистичной блокировкой записи, которая вызывает обновлениеPrescriptionВ строке (чтобы быть конкретным: его поле версии), когда новыйDose вставлен.

Кажется, это правильно.

Тем не менее, я предлагаю сначала проверить это ... более простой способ сделать это - отладка ... используя предпочитаемую IDE, установить точку отладки после предложения:

entityMgr.lock(p, LockModeType.OPTIMISTIC_FORCE_INCREMENT);

Позже попробуйте вызвать вашaddDose(prescriptionId) от двух разных клиентов, предоставляя один и тот же prescriptionID ... и позвольте одному клиенту закончить первым и посмотреть, что происходит с другим.

 Janus Varmarken10 авг. 2016 г., 01:54
Спасибо за предложение отладки. Я добавил +1. Тем не менее, я ищу несколько более сильное подтверждение правильности, чем «кажется правильным», чтобы принять ответ (например, ответ, который объясняет детали блокировки, специфичные для этого сценария).

Ваш ответ на вопрос