Pull to refresh

Как понять и подружиться с транзакциями и JPA

Reading time4 min
Views15K
Наверное, все знают о транзакциях в реляционных базах данных, все слышали про ACID. Но тем не менее есть разница между знать и прочувствовать, сам с этим столкнулся, когда пришлось переквалифицироваться в бэкэнд разработчика. Думаю, в тот момент подобная статья здорово бы мне помогла, надеюсь она окажется полезна и вам.

При разработке энтерпрайз приложений зачастую с базами данных взаимодействуют посредством ORM технологии, в мире джавы наиболее известна технология JPA (Java Persistence API) и её реализации — Hibernate и EclipseLink. JPA позволяет взаимодействовать с базой данных в терминах объектов предметной области, предоставляет кэш, репликацию кэша при наличии кластера в middle tier-е.

Как это обычно происходит:

  1. На бэкэнд приходит REST запрос обновить документ, в теле запроса — новое состояние.
  2. Начинаем транзакцию.
  3. Бэкэнд запрашивает существующее состояние документа у EntityManager-а, который может вычитать его из базы, а может достать из кэша.
  4. Далее мы берём объект прибывший в теле запроса смотрим на него, сравниваем с состоянием объекта представляющего запись в базе данных.
  5. На основе этого сравнения вносим необходимые изменения.
  6. Коммитим транзакцию.
  7. Возвращаем ответ клиенту.

Где здесь порылась собака? Смотрите, мы взяли данные, скорее всего из кэша, возможно уже протухшие, возможно сервер прямо сейчас обрабатывает конкурентный запрос на изменение того же документа, и данные протухают ровно в момент когда мы делаем все эти сравнения. На основе этих данных сомнительной достоверности и тела REST запроса мы принимаем решения о внесении изменений в базу и коммитим их. Тут встаёт вопрос, что за лажу мы только что записали в базу данных?

Здесь нам и помогут транзакции. Ключ к их пониманию — это при каких условиях транзакция не пройдёт, или, иначе говоря, когда случится её откат. А откат транзакции случится если вносимые изменения нарушат констрейнты базы данных. Наиболее важные из них:

  • Нарушение констрейнтов уникальности.
  • Нарушение ссылочной целостности.

И так, если наша транзакция прошла, то «лажа», которую мы закоммитили чуть выше, удовлетворяет констрейнтам. Осталось настроить констрейнты так, чтобы удовлетворяющие им данные представляли собой валидные бизнес-сущности.

Вот максимально примитивный и искусственный пример:

@Entity
public class Document {
    @Id
    private String name;
    
    @Lob
    private String content;

    // getters and setters пропущены
}

@ApplicationScoped
@Transactional // транзакции начнаются перед вызовом безнес-метода и завершаются по его окончанию
public class DocumentService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void createDocument(String name, String content) {
        // скорее всего никакого запроса к базе данных здесь не будет,
        // с большой долей вероятности мы получим закэшированный объект 
        Document documentEntity = entityManager.find(Document.class, name);
        
        if (documentEntity != null) {
            throw new WebApplicationException(Response.Status.CONFLICT); // конфликт имен!
        }

        // возможно прямо сейчас другой тред конкурентно создает документ с таким же именем
        documentEntity = new Document();
        documentEntity.setName(name);
        documentEntity.setContent(content);
        
        entityManager.persist(documentEntity);
    }
}

Здесь в случае кункурентного создания документа с тем же именем или если данные полученные из каша оказались устаревшими, в момент коммита случится ConstraintViolationException и бэкэнд вернет клиенту 500 ошибку. Пользователь повторит операцию чуть позже и получит вразумительное сообщение об ошибке или таки создаст документ.

На самом деле, 500 ошибки не очень желательны, фокус в том, что они почти никогда не будут случаться, ну а если специфика использования вашего приложения такова, что они случаются слишком часто, вот тогда стоит подумать о чём-нибудь более изощренном.

Попробуем что-нибудь посложнее. Допустим мы хотим иметь возможность защитить документ от удаления. Заводим новую таблицу:

@Entity
public class DocumentLock {
    @Id
    @GeneratedValue
    private Long id;
    
    @OneToOne
    private Document document;
    
    @Basic
    private String lockedBy;

    // getters, setters
}

И добавляем в класс Document:

    @OneToOne(mappedBy = "document")
    private DocumentLock lock;

Теперь чтобы защитить документ от удаления достаточно создать DocumentLock ссылающийся на документ. Логика удаляющая документ:

    public void deleteDocument(String name) {
        Document documentEntity = entityManager.find(Document.class, name);
        
        if (documentEntity == null) {
            throw new NotFoundException();
        }
        
        DocumentLock lock = documentEntity.getLock();
        
        if (lock != null) {
            throw new WebApplicationException(
                    "Document is locked by " + lock.getLockedBy(), 
                    Response.Status.BAD_REQUEST);
        }
        
        entityManager.remove(documentEntity);
    }

Смотрите, мы проверили, что лока нет, но использовали для это закэшированные данные возможно уже устаревшие, а возможно устаревающие прямо во время проверки. В этом случае наш код удаляя документ попытается нарушить ссылочную целостность данных и значит наша транзакция не пройдёт. Пара замечаний:

  1. Убедитесь, что каскадное удаление отключено, в случае каскадных удалений, удаление документа приведет к удалению всех записей, которые на него ссылаются. Т.е. наличие записи о бизнес-локе ничему не помешает.
  2. На самом деле код выше позволяет повесить несколько локов на один документ, т.е. требуется настроить ещё констрейнт уникальности.
  3. Пример сугубо синтетический, скорее всего имеет смысл поместить данные о владельце бизнес-лока прямо в документ, а не заводить отдельную таблицу. И затем использовать явный пессимистичный лок для проверки отсутствия этого бизнес-лока при удалении документа.

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

Подводя итоги: даже используя устаревшие и сомнительные данные (что вполне может иметь место при работе с БД посредством JPA) при принятии решения о внесении изменений в базу данных, и даже если конкурентно вносятся конфликтующие изменения, механизм транзакций не позволит нам сделать ничего, что нарушит ссылочную целостность либо не будет соответствовать наложенным констрейнтам, все действия объединённые данной транзакцией и приводящие к данному плачевному итогу будут отменены в соответствии с принципом атомарности. Просто имейте это ввиду моделируя данные и аккуратно расставляйте констрейнты.
Tags:
Hubs:
+10
Comments11

Articles

Change theme settings