feat: 약 소프트 삭제 도입 및 약물 충돌 정합성 정비 - #74
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough약물 충돌 응답에 약물 상세 객체를 추가했습니다. 충돌 재분석은 기존 결과를 갱신하고 stale 충돌을 정리합니다. 약물 삭제는 Changes약물 충돌 응답 구조
충돌 재분석 및 upsert
약물 소프트 삭제와 조회 범위
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant DrugConflictCommandService
participant DrugConflictRepository
participant DrugConflict
DrugConflictCommandService->>DrugConflictRepository: 약물 쌍에 해당하는 기존 충돌 조회
DrugConflictCommandService->>DrugConflict: severity와 conflictDescription 갱신
DrugConflictCommandService->>DrugConflictRepository: stale 충돌 삭제 또는 신규 충돌 저장
sequenceDiagram
participant MedicationCommandService
participant MedicationRepository
participant Medication
participant MedicationScheduleRepository
MedicationCommandService->>MedicationRepository: 삭제되지 않은 약물 조회
MedicationCommandService->>Medication: softDelete() 호출
MedicationScheduleRepository->>Medication: deletedAt와 hiddenFrom 기준 조회
Possibly related issues
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/test/java/com/piuda/callcare/domain/home/service/query/HomeCardQueryServiceTest.java (1)
75-75: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win삭제 날짜 경계를 영속성 테스트로 검증하세요.
Line 75의 mock은
hiddenFrom인자 전달만 검증합니다.MedicationScheduleRepository의m.deletedAt IS NULL OR m.deletedAt >= :hiddenFrom조건은 실행하지 않습니다.
@DataJpaTest에서 다음 경우를 검증하세요.
deletedAt == null이면 조회 결과에 포함됩니다.deletedAt이targetDate이면 조회 결과에서 제외됩니다.deletedAt이targetDate.plusDays(1).atStartOfDay()이면targetDate조회 결과에 포함됩니다.As per path instructions,
src/test/**/*.java: 테스트 커버리지를 확인한다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/java/com/piuda/callcare/domain/home/service/query/HomeCardQueryServiceTest.java` at line 75, Replace the mock-only boundary check in HomeCardQueryServiceTest with a `@DataJpaTest` covering MedicationScheduleRepository.findActiveSchedulesForHomeCards: verify records with deletedAt null are included, records deleted at targetDate are excluded, and records deleted at targetDate.plusDays(1).atStartOfDay() are included.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictResponse.java`:
- Around line 10-18:
src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictResponse.java:10-18에서
DrugConflictResponse의 drug1/drug2 중첩 응답 계약을 기준으로 목록 API의 새 JSON 구조를 반영하십시오.
src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/ConflictDrug.java:10-18의
drugType, drugName, prescriptionDate 등 중첩 필드를 사용하고 기존 평탄 필드는 제거한 뒤,
src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictDetailResponse.java:12-22와
src/main/java/com/piuda/callcare/domain/drugconflict/converter/DrugConflictConverter.java:18-60에서도
목록 및 상세 조회 모두 동일하게 매핑되도록 수정하십시오. API 문서와 예시 응답도 새 중첩 구조와 필드 삭제를 반영하고 프론트 소비자 변경을
배포에 맞추십시오.
In
`@src/main/java/com/piuda/callcare/domain/drugconflict/service/command/DrugConflictCommandService.java`:
- Around line 103-106: Update the DataIntegrityViolationException handling in
DrugConflictCommandService so concurrent drug-pair inserts are not swallowed
within the surrounding `@Transactional` transaction; use an atomic database upsert
or a separate REQUIRES_NEW transaction that re-reads the duplicate row and
updates severity and conflictDescription. Add an integration test covering two
transactions analyzing the same drug pair concurrently and verifying the
committed row contains the latest analysis result without
UnexpectedRollbackException.
---
Nitpick comments:
In
`@src/test/java/com/piuda/callcare/domain/home/service/query/HomeCardQueryServiceTest.java`:
- Line 75: Replace the mock-only boundary check in HomeCardQueryServiceTest with
a `@DataJpaTest` covering
MedicationScheduleRepository.findActiveSchedulesForHomeCards: verify records
with deletedAt null are included, records deleted at targetDate are excluded,
and records deleted at targetDate.plusDays(1).atStartOfDay() are included.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 529cd260-0a9e-4d2e-8306-4d480b80aa74
📒 Files selected for processing (18)
src/main/java/com/piuda/callcare/domain/drugconflict/converter/DrugConflictConverter.javasrc/main/java/com/piuda/callcare/domain/drugconflict/dto/response/ConflictDrug.javasrc/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictDetailResponse.javasrc/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictResponse.javasrc/main/java/com/piuda/callcare/domain/drugconflict/entity/DrugConflict.javasrc/main/java/com/piuda/callcare/domain/drugconflict/repository/DrugConflictRepository.javasrc/main/java/com/piuda/callcare/domain/drugconflict/service/command/DrugConflictCommandService.javasrc/main/java/com/piuda/callcare/domain/home/service/command/MedicationToggleCommandService.javasrc/main/java/com/piuda/callcare/domain/home/service/query/HomeCardQueryService.javasrc/main/java/com/piuda/callcare/domain/medication/controller/MedicationController.javasrc/main/java/com/piuda/callcare/domain/medication/entity/Medication.javasrc/main/java/com/piuda/callcare/domain/medication/repository/MedicationRepository.javasrc/main/java/com/piuda/callcare/domain/medication/repository/MedicationScheduleRepository.javasrc/main/java/com/piuda/callcare/domain/medication/service/command/MedicationCommandService.javasrc/main/java/com/piuda/callcare/domain/medication/service/query/MedicationQueryService.javasrc/test/java/com/piuda/callcare/domain/drugconflict/service/command/DrugConflictCommandServiceTest.javasrc/test/java/com/piuda/callcare/domain/home/service/command/MedicationToggleCommandServiceTest.javasrc/test/java/com/piuda/callcare/domain/home/service/query/HomeCardQueryServiceTest.java
| public record DrugConflictResponse( | ||
|
|
||
| @Schema(description = "충돌 ID") Long conflictId, | ||
| @Schema(description = "약1 ID") Long medicationId1, | ||
| @Schema(description = "약1 이름") String drugName1, | ||
| @Schema(description = "약2 ID") Long medicationId2, | ||
| @Schema(description = "약2 이름") String drugName2, | ||
| @Schema(description = "약1 정보") ConflictDrug drug1, | ||
| @Schema(description = "약2 정보") ConflictDrug drug2, | ||
| @Schema(description = "심각도 (CONTRAINDICATED=금기, CAUTION=주의)") ConflictSeverity severity, | ||
| @Schema(description = "심각도 한글 라벨") String severityLabel, | ||
| @Schema(description = "확인(해결) 여부") boolean isResolved | ||
| ) { |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# Find code and tests that still reference removed flat response fields.
rg -n -C 3 \
'DrugConflictResponse|DrugConflictDetailResponse|ConflictDrug|drug1Id|drug2Id|drug1Name|drug2Name' \
src/main/java src/test/java
# Locate controller and API-documentation tests for the affected endpoints.
rg -n -C 5 \
'getConflicts|getConflictDetail|/api/conflicts|MockMvc|WebTestClient' \
src/main/java src/test/javaRepository: PIUDAProject/Backend
Length of output: 41576
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect exact response DTOs and converter logic.
printf '--- DTO records ---\n'
cat -n src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictResponse.java
printf '\n--- ConflictDrug ---\n'
cat -n src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/ConflictDrug.java
printf '\n--- DrugConflictDetailResponse ---\n'
cat -n src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictDetailResponse.java
printf '\n--- Converter relevant section ---\n'
cat -n src/main/java/com/piuda/callcare/domain/drugconflict/converter/DrugConflictConverter.java | sed -n '1,80p'
# Read-only verifier: compare current public DTO public constructor/schema fields against removed flat fields.
python3 - <<'PY'
from pathlib import Path
import re
files = [
"src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictResponse.java",
"src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictDetailResponse.java",
"src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/ConflictDrug.java",
]
records = []
for path in files:
text = Path(path).read_text()
m = re.search(r'public record \w+\s*(.*?)\n\{', text, re.S)
if not m:
raise SystemExit(f"You need check {path}")
body = re.sub(r'`@Schema`\s*\(.*?\)\s*', '', m.group(1), flags=re.S)
fields = [re.sub(r'[,;\s]+.*','', f.strip()) for f in body.splitlines() if f.strip() and not f.strip().startswith('//')]
records.append((path.split('/')[-1], fields))
for name, fields in records:
print(name, fields)
flat_fields = ['drug1Id','drug2Id','drug1Name','drug2Name','drug1DrugType','drug2DrugType','drug1DrugNickname','drug2DrugNickname','drug1HospitalName','drug2HospitalName','drug1PrescriptionDate','drug2PrescriptionDate']
for name, fields in records:
removed = [f for f in fields if f.startswith('drug') or 'drugId' in f or f in flat_fields]
nested = [f for f in fields if f in ('drug1','drug2')]
if removed or nested:
print('root flat/nested fields:', name, removed or nested)
PY
printf '\n--- API tests using /api/conflicts or controller responses ---\n'
rg -n -C 4 '/api/conflicts|DrugConflictController|MockMvc|JsonPath|Extract.*Response|ApiResponse' src/test/java || trueRepository: PIUDAProject/Backend
Length of output: 249
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect exact response DTOs and converter logic.
echo '--- DTO records ---'
cat -n src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictResponse.java
echo
echo '--- ConflictDrug ---'
cat -n src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/ConflictDrug.java
echo
echo '--- DrugConflictDetailResponse ---'
cat -n src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictDetailResponse.java
echo
echo '--- Converter relevant section ---'
cat -n src/main/java/com/piuda/callcare/domain/drugconflict/converter/DrugConflictConverter.java | sed -n '1,80p'
# Read-only verifier: compare current public DTO public constructor/schema fields against removed flat fields.
python3 - <<'PY'
from pathlib import Path
import re
files = [
"src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictResponse.java",
"src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictDetailResponse.java",
"src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/ConflictDrug.java",
]
records = []
for path in files:
text = Path(path).read_text()
m = re.search(r'public record \w+\s*(.*?)\n\{', text, re.S)
if not m:
raise SystemExit(f"Check {path}")
body = re.sub(r'`@Schema`\s*\([^)]*\)\s*', '', m.group(1), flags=re.S)
fields = [re.sub(r'[,;\s]+.*','', f.strip()) for f in body.splitlines() if f.strip() and not f.strip().startswith('//')]
records.append((path.split('/')[-1], fields))
flat_fields = ['drug1Id','drug2Id','drug1Name','drug2Name','drug1DrugType','drug2DrugType','drug1DrugNickname','drug2DrugNickname','drug1HospitalName','drug2HospitalName','drug1PrescriptionDate','drug2PrescriptionDate']
for name, fields in records:
print(name, fields)
removed = [f for f in fields if f.startswith('drug') or 'drugId' in f or f in flat_fields]
nested = [f for f in fields if f in ('drug1','drug2')]
if removed or nested:
print('root flat/nested fields:', name, removed or nested)
PY
echo
echo '--- API tests using /api/conflicts or controller responses ---'
rg -n -C 4 '/api/conflicts|DrugConflictController|MockMvc|JsonPath|Extract.*Response|ApiResponse' src/test/java || trueRepository: PIUDAProject/Backend
Length of output: 6127
외부 API 응답 계약 변경을 배포 전에 확인하십시오.
약물 필드가 drug1/drug2 중첩 객체로 바뀌었습니다. 기존 클라이언트는 drug1Name, drug2Name 같은 평탄 필드를 더 읽지 못하므로 렌더링 오류가 발생할 수 있습니다.
GET /api/conflicts와/api/conflicts/{conflictId}응답을 새 중첩 JSON 구조로 매핑하는 프론트 변경을 배포와 맞추십시오.- API 문서와 예시 응답은 기존 필드 삭제와
drugType,drugName,prescriptionDate같은 중첩 필드 추가를 반영하십시오. - 이 변경은
GET /api/conflicts와 상세 조회 모두에 적용됩니다.
📍 Affects 4 files
src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictResponse.java#L10-L18(this comment)src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/ConflictDrug.java#L10-L18src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictDetailResponse.java#L12-L22src/main/java/com/piuda/callcare/domain/drugconflict/converter/DrugConflictConverter.java#L18-L60
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictResponse.java`
around lines 10 - 18,
src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictResponse.java:10-18에서
DrugConflictResponse의 drug1/drug2 중첩 응답 계약을 기준으로 목록 API의 새 JSON 구조를 반영하십시오.
src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/ConflictDrug.java:10-18의
drugType, drugName, prescriptionDate 등 중첩 필드를 사용하고 기존 평탄 필드는 제거한 뒤,
src/main/java/com/piuda/callcare/domain/drugconflict/dto/response/DrugConflictDetailResponse.java:12-22와
src/main/java/com/piuda/callcare/domain/drugconflict/converter/DrugConflictConverter.java:18-60에서도
목록 및 상세 조회 모두 동일하게 매핑되도록 수정하십시오. API 문서와 예시 응답도 새 중첩 구조와 필드 삭제를 반영하고 프론트 소비자 변경을
배포에 맞추십시오.
| } catch (DataIntegrityViolationException e) { | ||
| // 동시 요청으로 같은 조합이 먼저 저장된 경우(uk_drug_conflict_senior_med1_med2). 정합성은 제약이 보장하므로 무시. | ||
| // 동시 요청으로 같은 조합이 먼저 INSERT된 레이스(uk_drug_conflict_senior_med1_med2). | ||
| // 정합성은 UNIQUE 제약이 보장하므로 이번 요청은 저장을 건너뛴다(삼중 방어 유지). | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# Inspect the declared JPA/Hibernate versions and the current transaction path.
fd -HI '^(pom\.xml|build\.gradle|build\.gradle\.kts)$' . -x sh -c \
'echo "--- $1"; rg -n -C 2 "spring-boot|spring-data-jpa|hibernate" "$1"' sh {} || true
ast-grep outline \
src/main/java/com/piuda/callcare/domain/drugconflict/service/command/DrugConflictCommandService.java \
--view expanded
rg -n -C 4 \
'DataIntegrityViolationException|saveAndFlush|flush\(|`@Transactional`|findBySenior_IdAndMedication1_IdAndMedication2_Id' \
src/main/java src/test/javaRepository: PIUDAProject/Backend
Length of output: 37173
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "--- drug-conflict service"
cat -n src/main/java/com/piuda/callcare/domain/drugconflict/service/command/DrugConflictCommandService.java
echo "--- drug-conflict entity"
fd -HI 'DrugConflict\.java$' src/main/java -x sh -c 'echo "--- $1"; cat -n "$1"' sh {}
echo "--- build files"
cat -n build.gradle | sed -n '1,120p'
fd -HI '^(gradle\.properties|gradle-wrapper\.properties|libs\.versions\.toml)$' . -x sh -c 'echo "--- $1"; cat -n "$1"' sh {}Repository: PIUDAProject/Backend
Length of output: 12771
🌐 Web query:
Hibernate JPA data integrity violation unique constraint flush exception isRollbackOnly transaction rollback only
💡 Result:
In Hibernate and JPA, a data integrity violation—such as a unique constraint failure—typically occurs during the flush process, when the persistence context synchronizes its state with the database [1][2]. Because this violation puts the persistence context into an inconsistent or potentially corrupted state, Hibernate marks the current transaction as "rollback-only" [3]. When a transaction is marked as rollback-only, any attempt to commit it will fail, usually resulting in a javax.persistence.RollbackException or Spring's UnexpectedRollbackException [4][5][3]. Even if you catch the underlying DataIntegrityViolationException within your code, you cannot "save" the transaction; it is already doomed to be rolled back by the transaction manager [5][3]. Key Technical Realities: 1. Persistence Context Inconsistency: When a flush fails due to a constraint violation, Hibernate cannot guarantee the integrity of the session [3]. Consequently, the session is invalidated for the remainder of the transaction, and any subsequent attempts to use it will likely fail [3]. 2. Deferred Execution: Hibernate often delays SQL execution until a flush is triggered (e.g., at commit, during a query, or via a manual flush) [2]. This is why the exception is often thrown at the end of a method rather than the specific line of code that triggered the data change [4][6]. 3. Handling the Exception: - You cannot simply catch the exception to continue the same transaction [5][3]. - If you need to handle duplicate entries or constraint violations gracefully, the standard approach is to perform a check (e.g., repository.existsBy...) before attempting the operation [7]. - If you must attempt an operation that might fail, you can isolate it by using a separate transaction with propagation set to Propagation.REQUIRES_NEW [3]. This ensures the failure only affects the isolated transaction and does not mark the main transaction as rollback-only [3]. If you encounter the isRollbackOnly issue, it confirms that your application attempted to commit a transaction that encountered an unrecoverable error during the Hibernate flush phase [8][3]. To resolve this, redesign your logic to avoid relying on exceptions for flow control, or isolate risky operations into independent transactions [3][7].
Citations:
- 1: Delete event not flushed before Save in JpaRepository [DATAJPA-727] spring-projects/spring-data-jpa#1100
- 2: https://github.com/hibernate/hibernate-orm/blob/main/documentation/src/main/asciidoc/userguide/chapters/flushing/Flushing.adoc
- 3: https://stackoverflow.com/questions/63597445/avoid-unexpectedrollbackexception-with-spring-data-jpa
- 4: https://stackoverflow.com/questions/49497045/unable-to-catch-hibernate-exception-caused-by-constraints-violation
- 5: https://stackoverflow.com/questions/70203663/how-to-skip-the-rollback-for-dataintegrityviolationexception
- 6: https://stackoverflow.com/questions/21822663/db-constraint-violation-not-throwing-exception-in-hibernate
- 7: https://stackoverflow.com/questions/71750527/handling-specific-dataintegrityviolationexception-in-a-transaction
- 8: https://docs.spring.io/spring-framework/docs/5.0.11.RELEASE/javadoc-api/org/springframework/transaction/TransactionStatus.html
🌐 Web query:
Hibernate UNIQUE constraint violation unique key insert select insert exception
💡 Result:
A ConstraintViolationException occurs in Hibernate when a database operation violates a defined integrity constraint, such as a UNIQUE or PRIMARY KEY constraint [1][2]. Because Hibernate sessions are generally invalidated upon encountering such exceptions, the current transaction must be rolled back [3][4][5]. To handle scenarios involving potential unique constraint violations, consider the following approaches: 1. Use Upsert Logic (Recommended) The most robust solution to avoid duplicate key exceptions is to perform an atomic upsert operation rather than a select-then-insert pattern [6][4]. Since Hibernate 6.5, you can use the ON CONFLICT DO clause in JPQL/Criteria queries to handle conflicts in a database-agnostic or database-specific way (e.g., ON DUPLICATE KEY UPDATE in MySQL or MERGE in Oracle) [7]. 2. Defensive Checks You may perform a select query to check for the existence of the entity before attempting an insert [3][8]. Note that this is not atomic and can still fail due to race conditions in high-concurrency environments; using a transaction with an appropriate isolation level is necessary to mitigate this [3][4]. 3. Exception Handling If you must catch the exception, be aware of the following: - JPA often wraps the Hibernate ConstraintViolationException inside a PersistenceException [5]. - Because the session is invalidated, you cannot simply swallow the exception and continue using the same session [3][4]. You must rollback the transaction and, if necessary, start a new one to proceed [4]. - You can inspect the exception to identify the specific constraint violated using the getConstraintName and getKind methods, provided the database driver reports this information reliably [1][9][2]. Avoid the anti-pattern of relying solely on catching exceptions to control business logic, as it can be less performant and harder to maintain [8]. Always ensure that your database sequences or auto-increment configurations are correctly synchronized with existing data to prevent unintended primary key collisions [10][11].
Citations:
- 1: https://docs.hibernate.org/stable/orm/javadocs/org/hibernate/exception/ConstraintViolationException.html
- 2: https://docs.hibernate.org/orm/current/javadocs/org/hibernate/exception/ConstraintViolationException.ConstraintKind.html
- 3: https://stackoverflow.com/questions/2893969/hibernate-constraint-constraintviolationexception-is-there-an-easy-way-to-ignor
- 4: https://stackoverflow.com/questions/5022812/find-or-insert-based-on-unique-key-with-hibernate
- 5: https://stackoverflow.com/questions/74483731/why-cant-i-catch-constraintviolationexception-in-hibernate
- 6: https://discourse.hibernate.org/t/how-to-do-an-upsert-with-hibernate-and-mysql-and-avoid-throwing-duplicate-entry-for-key-issues/2349
- 7: https://vladmihalcea.com/hibernate-on-conflict-do-clause/
- 8: https://stackoverflow.com/questions/21721385/catching-org-hibernate-exception-constraintviolationexception
- 9: https://docs.hibernate.org/orm/current/javadocs/org/hibernate/exception/ConstraintViolationException.html
- 10: https://stackoverflow.com/questions/41530242/oracle-unique-constraint-violated-on-hibernate-insert
- 11: https://stackoverflow.com/questions/6918709/hibernate-constraintviolationexception-could-not-insert-sql-error
🌐 Web query:
MySQL InnoDB UNIQUE constraint violation deadlock duplicate insert wait lock unique index
💡 Result:
In MySQL InnoDB, deadlocks involving unique constraint violations are often caused by the specific way InnoDB handles locking when an INSERT statement encounters a duplicate key [1][2]. When a duplicate-key error occurs, InnoDB typically acquires a shared (S) lock on the existing duplicate index record [1][3]. This behavior is designed to ensure serializability and to guarantee that the information returned to the user—that a duplicate exists—is consistent with the database state at that moment [2]. The deadlock mechanism generally occurs as follows: 1. Multiple sessions attempt to insert the same unique key value [1][2]. 2. If one session already holds an exclusive (X) lock on the record (e.g., due to a prior operation like a DELETE or UPDATE), subsequent sessions attempting the insert will request a shared (S) lock on that same record due to the detected duplicate key [1][2]. 3. When the session holding the exclusive lock rolls back or completes, the queued requests for shared locks are granted [1][2]. 4. If multiple sessions now hold a shared lock and subsequently attempt to promote those locks to exclusive locks (or perform other operations requiring exclusive access) on the same record, they can deadlock because each session is waiting for the others to release their shared locks [1][2]. Additional factors that can contribute to these deadlocks include: - Next-Key Locking: InnoDB may apply next-key locks (a combination of a record lock and a gap lock) rather than simple record locks, which can block concurrent inserts in surrounding gaps, increasing the surface area for contention [1][4][5]. - Gap Locking with Multiple Unique Keys: When tables have multiple unique indexes, a duplicate key error on one index may trigger gap locks on the primary key or other secondary indexes to maintain consistency, further limiting concurrency [5][6]. - INSERT... ON DUPLICATE KEY UPDATE: While this statement behaves differently by acquiring exclusive locks rather than shared locks upon a collision, complex interaction with concurrent standard INSERT operations can still lead to deadlocks [1][7]. These behaviors are inherent to InnoDB's pessimistic locking approach to ensure data integrity and ACID compliance, particularly under high concurrency [8]. They are generally not considered bugs but expected behavior within the engine's locking architecture [5][8]. To mitigate these issues, it is recommended to ensure application code is prepared to handle deadlock errors (e.g., via retries) and to optimize access patterns to avoid frequent collisions on the same unique keys [7][6].
Citations:
- 1: https://dev.mysql.com/doc/refman/8.4/en/innodb-locks-set.html
- 2: https://bugs.mysql.com/bug.php?id=35821
- 3: https://dba.stackexchange.com/questions/286061/why-does-insert-on-duplicate-key-error-create-a-share-lock
- 4: https://www.alibabacloud.com/blog/mysql-unique-check-issue_600813
- 5: https://bugs.mysql.com/bug.php?id=116503
- 6: https://bugs.mysql.com/bug.php?id=116815
- 7: https://bugs.mysql.com/bug.php?id=52020
- 8: https://bugs.mysql.com/99736
🌐 Web query:
MySQL 2024-2025 duplicate key insert lock lock wait timeout gap lock concurrent insert unique index
💡 Result:
In MySQL's InnoDB storage engine, a lock wait timeout during concurrent INSERT operations involving unique indexes is often caused by the locking behavior triggered by duplicate-key errors [1][2][3][4]. When a session attempts an INSERT and encounters a duplicate-key error (due to a violation of a UNIQUE or PRIMARY KEY constraint), InnoDB sets a lock on the existing duplicate index record [5][3]. For simple INSERT statements, this is typically a shared (S) lock, whereas INSERT... ON DUPLICATE KEY UPDATE sets an exclusive (X) lock [5][1]. The key technical factors causing lock wait timeouts in concurrent environments include: 1. Next-Key Locking and Gap Locks: While a standard INSERT primarily uses insert intention locks (which allow concurrency), a duplicate-key error can cause InnoDB to escalate to a next-key lock (index record lock plus a gap lock) on the duplicate record [5][1]. This gap lock prevents other sessions from inserting into the gap before the record, potentially blocking unrelated concurrent inserts [6][3][4]. 2. Tables with Multiple Unique Indexes: When a table has multiple unique indexes, an insert might successfully pass one index check but trigger a duplicate-key error on another [2]. This behavior can result in unexpected gap locks on indexes that were not intended to be locked, leading to wide-ranging blocking of other INSERT operations and subsequent lock wait timeouts for unrelated transactions [2][3][4]. 3. Deadlocks and Lock Wait Timeouts: If multiple sessions attempt to insert the same duplicate value simultaneously, they may all request locks on the same record [5]. If one session holds an exclusive lock and others request shared locks (or vice-versa), it can lead to deadlocks or prolonged waiting periods that exceed the innodb_lock_wait_timeout setting [5][1][4]. To mitigate these issues, it is generally recommended to avoid using ON DUPLICATE KEY UPDATE on tables with multiple unique indexes [2]. Furthermore, because gap locks are necessary for maintaining data integrity (specifically to prevent phantom rows and enforce uniqueness constraints in transactional isolation), they cannot be entirely disabled [3][7][8]. If you encounter frequent lock wait timeouts, analyze your transaction patterns to reduce the time that locks are held and consider whether your application logic can be structured to minimize contention on highly active unique indexes [4][9].
Citations:
- 1: https://stackoverflow.com/questions/63270714/why-mysql-innodb-set-an-s-or-x-next-key-lock-on-the-duplicate-index-record-when
- 2: https://bugs.mysql.com/bug.php?id=111703
- 3: https://bugs.mysql.com/bug.php?id=116503
- 4: https://bugs.mysql.com/bug.php?id=116815
- 5: https://dev.mysql.com/doc/refman/5.7/en/innodb-locks-set.html
- 6: https://dev.mysql.com/doc/refman/9.1/en/innodb-next-key-locking.html
- 7: https://dev.mysql.com/doc/en/innodb-locking.html
- 8: https://dev.mysql.com/doc/refman/9.1/en/innodb-locking.html
- 9: https://bugs.mysql.com/99736
동시 INSERT 충돌을 현재 트랜잭션에서 무시하지 마십시오.
@Transactional 메서드에서 발생한 DataIntegrityViolationException은 일반적으로 트랜잭션을 rollback-only로 만들고, 이후 commit 시 UnexpectedRollbackException으로 실패할 수 있습니다. duplicate row가 commit되면 severity/conflictDescription도 현재 요청의 최신 결과로 갱신되지 않습니다. DB 원자적 upsert 또는 REQUIRES_NEW 단위로 재조회 후 갱신하는 별도 트랜잭션 흐름으로 변경하고, 두 트랜잭션에서 같은 약물 쌍을 동시 분석하는 통합 케이스를 추가하십시오.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/piuda/callcare/domain/drugconflict/service/command/DrugConflictCommandService.java`
around lines 103 - 106, Update the DataIntegrityViolationException handling in
DrugConflictCommandService so concurrent drug-pair inserts are not swallowed
within the surrounding `@Transactional` transaction; use an atomic database upsert
or a separate REQUIRES_NEW transaction that re-reads the duplicate row and
updates severity and conflictDescription. Add an integration test covering two
transactions analyzing the same drug pair concurrently and verifying the
committed row contains the latest analysis result without
UnexpectedRollbackException.
PR Description — feature/71
제목
본문
무엇을 / 왜
약물 충돌 분석 결과가 원천 데이터를 따라가지 못하는 정합성 문제를 정리하고,
그 과정에서 드러난 약 삭제 방식(하드 삭제 → soft delete)을 함께 전환했습니다.
주요 변경
1. 충돌 재분석 시 기존 행 갱신 (skip → upsert)
이미 저장된 조합을 건너뛰어, 약 정보가 바뀌어 위험도가 "주의 → 금기"로 올라가도 옛 등급이 그대로 노출됐습니다.
값이 실제로 달라진 경우에만 갱신하도록 변경했습니다.
2. 충돌 카드에 표시 정보 4종 추가
제품종류·제품명·처방기관·처방날짜를 내려줍니다. 저장하지 않고 조회 시 Medication에서 합성해
약을 수정하면 카드에도 즉시 반영됩니다(기존 fetch join 활용 — 추가 쿼리 없음).
처방날짜는
prescription_date(OCR) → 없으면start_date(사용자 입력) 폴백.3. 약 삭제를 soft delete로 전환
medication_log등이 FK로 물려 있어 하드 삭제 시 복약 이력이 파괴되는 문제가 있었습니다.deleted_at만 채우고 참조 행은 보존하며, 노출 규칙은 "삭제일 당일부터 앞으로만 숨김" 입니다.→ 오늘·미래 홈카드와 약물노트·충돌 목록에서는 빠지고,
삭제 이전 날짜의 홈카드와 복약 기록 리포트에는 그대로 남습니다.
4. 유효하지 않은 충돌 숨김
복용이 끝났거나 삭제된 약의 충돌은 목록·상세 모두에서 노출되지 않습니다(두 화면 기준 통일).
GET /api/conflicts응답 구조가 바뀝니다. 배포 순서 조율이 필요합니다.DB
medication.deleted_at컬럼 추가 —ddl-auto: update로 자동 반영되며 기존 행은NULL(유효)입니다.테스트
CallcareApplicationTests)은 로컬 MySQL 미기동에 따른컨텍스트 로드 실패로, 이번 변경 이전부터 있던 동일 실패입니다.
구조 변경 후 깨져 있던
HomeCardQueryServiceTest픽스처를 복구했습니다.리뷰 포인트
상태값이고 이력은 리포트가 담당한다고 보았는데, 다르게 보신다면 의견 주세요.
Summary by CodeRabbit
새 기능
개선