Как исключить наложение изменений документа пользователями в базе данных
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Как исключить наложение изменений документа пользователями в базе данных
Эта проблема известна как race condition или конфликт конкурентных изменений. Когда несколько пользователей одновременно редактируют один документ, возникает риск потери данных или перезаписи важной информации. Рассмотрим несколько подходов.
1. Оптимистичная блокировка (Optimistic Locking)
Это наиболее распространённый подход в Java приложениях. Используется поле версии (version field):
@Entity
@Table(name = "documents")
public class Document {
@Id
@GeneratedValue
private Long id;
@Version
private Long version; // Автоматически управляется Hibernate
private String title;
private String content;
}
При обновлении Hibernate добавляет условие:
UPDATE documents
SET title = ?, content = ?, version = version + 1
WHERE id = ? AND version = ?
Если version не совпадает, выбросится OptimisticLockException — это означает, что документ изменился.
@Service
public class DocumentService {
@Transactional
public void updateDocument(Long id, String newTitle) {
Document doc = documentRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Document not found"));
doc.setTitle(newTitle);
// При сохранении Hibernate проверит версию
try {
documentRepository.save(doc);
} catch (OptimisticLockException e) {
throw new BusinessException("Document was modified by another user. Please refresh and try again.");
}
}
}
2. Пессимистичная блокировка (Pessimistic Locking)
Ушу документ блокируется при чтении, чтобы другие не могли его изменять:
@Repository
public interface DocumentRepository extends JpaRepository<Document, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT d FROM Document d WHERE d.id = :id")
Optional<Document> findByIdForUpdate(@Param("id") Long id);
}
@Service
public class DocumentService {
@Transactional
public void updateDocument(Long id, String newTitle) {
Document doc = documentRepository.findByIdForUpdate(id)
.orElseThrow(() -> new EntityNotFoundException("Document not found"));
// Документ заблокирован на уровне БД
doc.setTitle(newTitle);
documentRepository.save(doc);
}
}
В PostgreSQL это генерирует:
SELECT * FROM documents WHERE id = ? FOR UPDATE;
3. Временные метки (Timestamps)
Добавьте поле updated_at и сравнивайте его:
@Entity
@Table(name = "documents")
public class Document {
@Id
private Long id;
private String title;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
}
@Service
public class DocumentService {
@Transactional
public void updateDocument(Long id, String newTitle, Instant clientUpdatedAt) {
Document doc = documentRepository.findById(id)
.orElseThrow();
// Проверяем, не изменился ли документ с момента загрузки
if (!doc.getUpdatedAt().equals(clientUpdatedAt)) {
throw new BusinessException("Document was modified. Current version: " + doc.getUpdatedAt());
}
doc.setTitle(newTitle);
doc.setUpdatedAt(Instant.now());
documentRepository.save(doc);
}
}
4. ETag HTTP header
Для REST API используйте ETag для отслеживания изменений:
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
@GetMapping("/{id}")
public ResponseEntity<DocumentDTO> getDocument(@PathVariable Long id) {
Document doc = documentService.getDocument(id);
String etag = generateETag(doc);
return ResponseEntity.ok()
.header("ETag", etag)
.body(mapToDTO(doc));
}
@PutMapping("/{id}")
public ResponseEntity<DocumentDTO> updateDocument(
@PathVariable Long id,
@RequestBody DocumentDTO dto,
@RequestHeader("If-Match") String clientETag) {
Document doc = documentService.getDocument(id);
String currentETag = generateETag(doc);
if (!clientETag.equals(currentETag)) {
return ResponseEntity.status(HttpStatus.CONFLICT).build();
}
documentService.updateDocument(id, dto);
return ResponseEntity.ok(mapToDTO(documentService.getDocument(id)));
}
private String generateETag(Document doc) {
return Integer.toHexString(Objects.hash(doc.getId(), doc.getVersion()));
}
}
5. Merge/Diff алгоритмы (для сложных случаев)
Если нужно объединить изменения от нескольких пользователей (как в Google Docs):
public class DocumentMergeService {
public String mergeChanges(String original, String version1, String version2) {
// Используйте библиотеку diff-match-patch
diff_match_patch dmp = new diff_match_patch();
LinkedList<diff_match_patch.diff_match_patch.Diff> diffs1 =
dmp.diff_main(original, version1);
LinkedList<diff_match_patch.Diff> diffs2 =
dmp.diff_main(original, version2);
// Объединяем изменения
return mergePatches(original, diffs1, diffs2);
}
}
6. Лучшие практики
Выберите подходящий метод:
- Оптимистичная блокировка — лучший выбор для большинства случаев (меньше lock contention)
- Пессимистичная блокировка — если конфликты редкие или нужна гарантия
- ETag — для REST API
- Diff merge — для сложных документов
Уведомляйте пользователя:
if (exception instanceof OptimisticLockException) {
return new ErrorResponse(
"Document was changed by another user. Please refresh your browser.",
HttpStatus.CONFLICT.value()
);
}
Добавьте аудит:
@Entity
public class DocumentAudit {
private Long documentId;
private String changedBy;
private String oldValue;
private String newValue;
private Instant changedAt;
}
Сравнение методов
| Метод | Производительность | Сложность | Случаи использования |
|---|---|---|---|
| Оптимистичная блокировка | Высокая | Низкая | Большинство приложений |
| Пессимистичная блокировка | Средняя | Средняя | Критичные данные |
| ETag | Высокая | Низкая | REST API |
| Timestamps | Высокая | Низкая | Простые проверки |
| Diff merge | Низкая | Высокая | Collaborative editing |
Вывод
Для типичного Java приложения рекомендую оптимистичную блокировку с аннотацией @Version. Она надёжна, эффективна и хорошо интегрирована с Hibernate. Для критических операций добавьте пессимистичную блокировку или дополнительную валидацию.