← Назад к вопросам

Как исключить наложение изменений документа пользователями в базе данных

2.0 Middle🔥 111 комментариев
#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Как исключить наложение изменений документа пользователями в базе данных

Эта проблема известна как 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. Для критических операций добавьте пессимистичную блокировку или дополнительную валидацию.

Как исключить наложение изменений документа пользователями в базе данных | PrepBro