Skip to content

Commit 62e069f

Browse files
committed
fix(core): inject soft-delete conditions into tree native SQL queries
Tree repository native queries (CTE recursive, direct children, root items, child count, etc.) bypassed Hibernate @filter, causing soft-deleted records to appear in results. This fix auto-detects @filter annotation on entities implementing SoftDeletable and injects AND deleted = false conditions into all 14 query methods in TreeQueries. - Add softDeleteColumn parameter to TreeQueries with 3 helper methods - Extract soft-delete column from @filter annotation in RepositoryFactoryBean - Apply conditions to CTE anchor/recursive members across all DB dialects - Add unit tests for SQL generation and integration tests for query results - Update tree-structure, overview, and README documentation
1 parent 6dcd924 commit 62e069f

10 files changed

Lines changed: 1256 additions & 96 deletions

File tree

simplix-core/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ SimpliX 프레임워크의 핵심 모듈입니다. 다른 모든 SimpliX 모듈
55
## Features
66

77
-**베이스 엔티티/리포지토리** - SimpliXBaseEntity, SimpliXBaseRepository
8-
-**계층 구조(트리) 지원** - TreeEntity, SimpliXTreeService (40+ 메서드)
8+
-**계층 구조(트리) 지원** - TreeEntity, SimpliXTreeService (40+ 메서드), SoftDeletable 자동 필터링
99
-**타입 변환 시스템** - Boolean, Enum, DateTime 변환기
1010
-**XSS/SQL Injection 방지** - OWASP 기반 HtmlSanitizer, SqlInjectionValidator
1111
-**민감 데이터 마스킹** - DataMaskingUtils, LogMasker, IpAddressMaskingUtils

simplix-core/docs/ko/overview.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ flowchart TB
2121
T1[TreeEntity]
2222
T2[SimpliXTreeRepository]
2323
T3[SimpliXTreeService]
24+
T4["SoftDeletable 통합"]
2425
end
2526
2627
subgraph CONVERT["타입 변환"]
@@ -99,6 +100,8 @@ simplix-core/
99100
├── tree/ # 트리 구조 지원
100101
│ ├── entity/
101102
│ │ └── TreeEntity.java
103+
│ ├── base/
104+
│ │ └── TreeQueries.java # DB별 네이티브 쿼리 생성 (soft-delete 자동 포함)
102105
│ ├── repository/
103106
│ │ ├── SimpliXTreeRepository.java
104107
│ │ └── SimpliXTreeRepositoryImpl.java
@@ -198,6 +201,7 @@ simplix-core/
198201
| **SimpliXTreeRepository<T, ID>** | tree.repository | 트리 전용 리포지토리 |
199202
| **SimpliXTreeService<T, ID>** | tree.service | 트리 CRUD, 탐색, 조작, 분석 서비스 |
200203
| **@TreeEntityAttributes** | tree.annotation | 트리 엔티티 메타데이터 설정 |
204+
| **SoftDeletable** | entity | Soft-delete 마커 인터페이스 (트리 네이티브 쿼리 자동 필터링) |
201205
| **BooleanConverter** | convert.bool | Boolean ↔ String 변환 |
202206
| **EnumConverter** | convert.enumeration | Enum ↔ String/Map 변환 |
203207
| **DateTimeConverter** | convert.datetime | 날짜/시간 타입 변환 |

simplix-core/docs/ko/tree-structure.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ SimpliX Core는 계층 구조(트리) 데이터를 위한 포괄적인 지원을
88
flowchart LR
99
TE["TreeEntity&lt;T,ID&gt;<br/>- getId()<br/>- getParentId()<br/>- getChildren()<br/>- getSortKey()"]
1010
TEA["@TreeEntityAttributes<br/>- tableName<br/>- lookupColumns<br/>- cascadeDelete"]
11+
SD["SoftDeletable<br/>- isDeleted()<br/>+ @Filter 자동 감지"]
1112
STS["SimpliXTreeService<br/>- 40+ methods<br/>- 8 categories"]
1213
1314
TE --- TEA
15+
TE --- SD
1416
TEA --- STS
17+
SD --- STS
1518
```
1619

1720
---
@@ -664,6 +667,141 @@ Electronics (id=1, parentId=null)
664667

665668
---
666669

670+
## Soft-Delete 통합
671+
672+
트리 구조는 `SoftDeletable` 인터페이스와 Hibernate `@Filter` 어노테이션을 기반으로 soft-delete 필터링을 자동 지원합니다.
673+
모든 네이티브 SQL 쿼리(계층 조회, 자식 조회, 루트 조회, 카운트 등)에서 soft-delete된 레코드가 자동으로 제외됩니다.
674+
675+
### 동작 원리
676+
677+
```mermaid
678+
flowchart TD
679+
A["엔티티 클래스"] --> B{"SoftDeletable<br/>구현 여부?"}
680+
B -->|아니오| C["일반 쿼리 생성<br/>(soft-delete 조건 없음)"]
681+
B -->|예| D["@Filter 어노테이션<br/>파싱"]
682+
D --> E["soft-delete 컬럼명<br/>자동 추출"]
683+
E --> F["모든 네이티브 쿼리에<br/>AND deleted = false 삽입"]
684+
```
685+
686+
### 설정 방법
687+
688+
엔티티에 다음 3가지를 설정하면 자동으로 soft-delete 필터링이 적용됩니다:
689+
690+
1. `SoftDeletable` 인터페이스 구현
691+
2. Hibernate `@FilterDef` / `@Filter` 어노테이션 선언
692+
3. `deleted` 컬럼 정의
693+
694+
```java
695+
@Entity
696+
@Table(name = "departments")
697+
@FilterDef(
698+
name = "softDeleteFilter",
699+
parameters = @ParamDef(name = "isDeleted", type = Boolean.class)
700+
)
701+
@Filter(name = "softDeleteFilter", condition = "deleted = :isDeleted")
702+
@TreeEntityAttributes(
703+
tableName = "departments",
704+
idColumn = "id",
705+
parentIdColumn = "parent_id",
706+
sortOrderColumn = "sort_order"
707+
)
708+
@Getter
709+
@Setter
710+
public class Department implements TreeEntity<Department, Long>, SoftDeletable {
711+
712+
@Id
713+
@GeneratedValue(strategy = GenerationType.IDENTITY)
714+
private Long id;
715+
716+
@Column(name = "parent_id")
717+
private Long parentId;
718+
719+
@Column(name = "sort_order")
720+
private Integer sortOrder;
721+
722+
@Column(name = "deleted", nullable = false)
723+
private Boolean deleted = false;
724+
725+
@Transient
726+
private List<Department> children = new ArrayList<>();
727+
728+
@Override
729+
public Comparable<?> getSortKey() {
730+
return sortOrder;
731+
}
732+
733+
@Override
734+
public boolean isDeleted() {
735+
return Boolean.TRUE.equals(deleted);
736+
}
737+
}
738+
```
739+
740+
### 자동 감지 메커니즘
741+
742+
`SimpliXRepositoryFactoryBean`이 리포지토리 생성 시 다음 단계를 자동 수행합니다:
743+
744+
1. 엔티티가 `SoftDeletable` 인터페이스를 구현하는지 확인
745+
2. `@Filter` 어노테이션의 `condition` 속성에서 컬럼명 추출 (예: `"deleted = :isDeleted"` -> `"deleted"`)
746+
3. 추출된 컬럼명을 `TreeQueries`에 전달하여 모든 네이티브 쿼리에 조건 자동 삽입
747+
748+
> **Note**: 별도의 어노테이션 속성 추가 없이 기존 `@Filter` 설정을 그대로 활용합니다.
749+
750+
### 쿼리별 soft-delete 조건 적용
751+
752+
| 쿼리 메서드 | 적용 위치 |
753+
|-------------|-----------|
754+
| `findDirectChildren()` | WHERE 절 |
755+
| `findRootItems()` | WHERE 절 |
756+
| `findCompleteHierarchy()` | CTE anchor + recursive member |
757+
| `findItemWithAllDescendants()` | CTE anchor + recursive member |
758+
| `findAncestors()` | CTE recursive member (anchor 제외) |
759+
| `findSiblings()` | WHERE 절 |
760+
| `findLeafNodes()` | 외부 쿼리 + 서브쿼리 |
761+
| `countChildrenByParentId()` | WHERE 절 |
762+
| `findRootItemsWithChildCount()` | 외부 쿼리 + 서브쿼리 |
763+
| `findDirectChildrenWithChildCount()` | 외부 쿼리 + 서브쿼리 |
764+
| `findByLookup()` | WHERE 절 |
765+
766+
### CTE 재귀 쿼리에서의 적용
767+
768+
CTE(Common Table Expression) 기반 재귀 쿼리는 anchor member와 recursive member 모두에 soft-delete 조건이 적용됩니다:
769+
770+
```sql
771+
-- PostgreSQL/MySQL 예시 (findCompleteHierarchy)
772+
WITH RECURSIVE hierarchy AS (
773+
-- Anchor: 루트 노드 (soft-delete 필터링)
774+
SELECT *, 1 as level
775+
FROM departments WHERE (parent_id IS NULL OR TRIM(parent_id) = '')
776+
AND deleted = false
777+
UNION ALL
778+
-- Recursive: 자식 노드 (soft-delete 필터링)
779+
SELECT c.*, h.level + 1
780+
FROM departments c
781+
JOIN hierarchy h ON c.parent_id = h.id
782+
AND c.deleted = false
783+
)
784+
SELECT hierarchy.* FROM hierarchy ORDER BY ...
785+
```
786+
787+
> **Note**: `findAncestors()` 쿼리는 anchor member(시작 노드)에는 soft-delete 조건을 적용하지 않습니다.
788+
> 시작 노드는 이미 알려진 노드이므로, 재귀 탐색(부모 방향)에서만 soft-delete된 조상을 제외합니다.
789+
790+
### 하위 호환성
791+
792+
`SoftDeletable`을 구현하지 않는 기존 엔티티는 이전과 동일하게 동작합니다.
793+
soft-delete 조건이 null인 경우 쿼리에 추가 조건이 삽입되지 않습니다.
794+
795+
### 알려진 제한사항
796+
797+
| 제한사항 | 설명 |
798+
|----------|------|
799+
| CTE fallback | CTE를 지원하지 않는 DB에서는 `findAll()` fallback이 실행되며, 이 경우 soft-delete 필터링이 적용되지 않음 |
800+
| JPA 메서드 | `findAll()`, `findById()` 등 Spring Data JPA 기본 메서드는 네이티브 쿼리가 아니므로 이 기능의 영향을 받지 않음. JPA 레벨 필터링은 Hibernate `@Filter` 활성화 또는 `@SQLRestriction` 사용 필요 |
801+
| 컬럼명 규칙 | `@Filter` condition이 `컬럼명 = :파라미터` 형식을 따라야 함 |
802+
803+
---
804+
667805
## 성능 고려사항
668806

669807
### Lazy Loading

0 commit comments

Comments
 (0)