@simplysm/orm-common 13.0.95 → 13.0.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/README.md +0 -112
- package/docs/ddl.md +0 -195
- package/docs/query.md +0 -469
- package/docs/schema.md +0 -238
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/orm-common",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.97",
|
|
4
4
|
"description": "Simplysm Package - ORM Module (common)",
|
|
5
5
|
"author": "simplysm",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -20,6 +20,6 @@
|
|
|
20
20
|
],
|
|
21
21
|
"sideEffects": false,
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@simplysm/core-common": "13.0.
|
|
23
|
+
"@simplysm/core-common": "13.0.97"
|
|
24
24
|
}
|
|
25
25
|
}
|
package/README.md
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# @simplysm/orm-common
|
|
2
|
-
|
|
3
|
-
플랫폼 중립적인 ORM 핵심 모듈. 스키마 정의, 타입 안전한 쿼리 빌더, DDL 관리, MySQL/PostgreSQL/MSSQL 방언 지원.
|
|
4
|
-
|
|
5
|
-
## 설치
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @simplysm/orm-common
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
**의존성:** `@simplysm/core-common`
|
|
12
|
-
|
|
13
|
-
## 문서
|
|
14
|
-
|
|
15
|
-
| 카테고리 | 설명 |
|
|
16
|
-
|---------|------|
|
|
17
|
-
| [스키마 정의](docs/schema.md) | Table, View, Procedure, Column, Relation, Index 빌더 |
|
|
18
|
-
| [쿼리 & 표현식](docs/query.md) | Queryable, Executable, expr 표현식 빌더 |
|
|
19
|
-
| [DDL & 초기화](docs/ddl.md) | DDL 메서드, 스키마 초기화, 마이그레이션 |
|
|
20
|
-
|
|
21
|
-
## 빠른 시작
|
|
22
|
-
|
|
23
|
-
### DbContext 정의
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
import { defineDbContext, createDbContext, Table, expr } from "@simplysm/orm-common";
|
|
27
|
-
|
|
28
|
-
// 테이블 정의
|
|
29
|
-
const User = Table("user")
|
|
30
|
-
.columns((c) => ({
|
|
31
|
-
id: c.int().autoIncrement(),
|
|
32
|
-
name: c.varchar(100),
|
|
33
|
-
email: c.varchar(200).nullable(),
|
|
34
|
-
createdAt: c.datetime(),
|
|
35
|
-
}))
|
|
36
|
-
.primaryKey("id")
|
|
37
|
-
.indexes((i) => [i.index("email").unique()]);
|
|
38
|
-
|
|
39
|
-
const Order = Table("order")
|
|
40
|
-
.columns((c) => ({
|
|
41
|
-
id: c.int().autoIncrement(),
|
|
42
|
-
userId: c.int(),
|
|
43
|
-
amount: c.decimal(10, 2),
|
|
44
|
-
}))
|
|
45
|
-
.primaryKey("id")
|
|
46
|
-
.relations((r) => ({
|
|
47
|
-
user: r.foreignKey(["userId"], () => User),
|
|
48
|
-
}));
|
|
49
|
-
|
|
50
|
-
// DbContext 정의 (스키마 블루프린트)
|
|
51
|
-
const MyDb = defineDbContext({
|
|
52
|
-
tables: { user: User, order: Order },
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// DbContext 인스턴스 생성 (런타임)
|
|
56
|
-
const db = createDbContext(MyDb, executor, { database: "mydb" });
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### 쿼리 실행
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
await db.connect(async () => {
|
|
63
|
-
// SELECT
|
|
64
|
-
const users = await db.user()
|
|
65
|
-
.where((c) => [expr.eq(c.name, "Alice")])
|
|
66
|
-
.orderBy((c) => c.createdAt, "DESC")
|
|
67
|
-
.execute();
|
|
68
|
-
|
|
69
|
-
// JOIN (관계 기반)
|
|
70
|
-
const orders = await db.order()
|
|
71
|
-
.include((c) => c.user)
|
|
72
|
-
.where((c) => [expr.gt(c.amount, 100)])
|
|
73
|
-
.execute();
|
|
74
|
-
|
|
75
|
-
// 집계
|
|
76
|
-
const stats = await db.order()
|
|
77
|
-
.select((c) => ({
|
|
78
|
-
userId: c.userId,
|
|
79
|
-
total: expr.sum(c.amount),
|
|
80
|
-
count: expr.count(),
|
|
81
|
-
}))
|
|
82
|
-
.groupBy((c) => [c.userId])
|
|
83
|
-
.execute();
|
|
84
|
-
|
|
85
|
-
// INSERT
|
|
86
|
-
await db.user().insert([{ name: "Bob", email: "bob@example.com", createdAt: new DateTime() }]);
|
|
87
|
-
|
|
88
|
-
// INSERT 후 ID 반환
|
|
89
|
-
const [inserted] = await db.user().insert(
|
|
90
|
-
[{ name: "Charlie", createdAt: new DateTime() }],
|
|
91
|
-
["id"],
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
// UPDATE
|
|
95
|
-
await db.user()
|
|
96
|
-
.where((c) => [expr.eq(c.id, 1)])
|
|
97
|
-
.update((c) => ({ name: expr.val("string", "Alice2") }));
|
|
98
|
-
|
|
99
|
-
// DELETE
|
|
100
|
-
await db.user()
|
|
101
|
-
.where((c) => [expr.eq(c.id, 1)])
|
|
102
|
-
.delete();
|
|
103
|
-
});
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### 지원 방언
|
|
107
|
-
|
|
108
|
-
| 방언 | 값 | 최소 버전 |
|
|
109
|
-
|------|-----|----------|
|
|
110
|
-
| MySQL | `"mysql"` | 8.0.14+ |
|
|
111
|
-
| MSSQL | `"mssql"` | 2012+ |
|
|
112
|
-
| PostgreSQL | `"postgresql"` | 9.0+ |
|
package/docs/ddl.md
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
# DDL & 초기화
|
|
2
|
-
|
|
3
|
-
`createDbContext()`로 생성된 DbContext 인스턴스에서 사용하는 DDL 메서드.
|
|
4
|
-
|
|
5
|
-
**중요:** DDL 연산은 트랜잭션 내에서 실행할 수 없다. `connectWithoutTransaction()` 내에서 사용해야 한다.
|
|
6
|
-
|
|
7
|
-
## 스키마 초기화
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
await db.connectWithoutTransaction(async () => {
|
|
11
|
-
// 정의된 모든 테이블/뷰/프로시저 자동 생성
|
|
12
|
-
await db.initialize();
|
|
13
|
-
|
|
14
|
-
// 기존 스키마 삭제 후 재생성
|
|
15
|
-
await db.initialize({ force: true });
|
|
16
|
-
|
|
17
|
-
// 특정 DB만 초기화
|
|
18
|
-
await db.initialize({ dbs: ["mydb"] });
|
|
19
|
-
});
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## 테이블 DDL
|
|
23
|
-
|
|
24
|
-
```typescript
|
|
25
|
-
// 테이블 생성 (TableBuilder 전달)
|
|
26
|
-
await db.createTable(User);
|
|
27
|
-
|
|
28
|
-
// 테이블 삭제 (QueryDefObjectName 전달)
|
|
29
|
-
await db.dropTable({ name: "user", database: "mydb" });
|
|
30
|
-
|
|
31
|
-
// 테이블 이름 변경
|
|
32
|
-
await db.renameTable({ name: "user" }, "users_v2");
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## 컬럼 DDL
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
import { createColumnFactory } from "@simplysm/orm-common";
|
|
39
|
-
const c = createColumnFactory();
|
|
40
|
-
|
|
41
|
-
// 컬럼 추가
|
|
42
|
-
await db.addColumn({ name: "user" }, "phone", c.varchar(20).nullable());
|
|
43
|
-
|
|
44
|
-
// 컬럼 수정
|
|
45
|
-
await db.modifyColumn({ name: "user" }, "phone", c.varchar(50).nullable());
|
|
46
|
-
|
|
47
|
-
// 컬럼 이름 변경
|
|
48
|
-
await db.renameColumn({ name: "user" }, "phone", "phoneNumber");
|
|
49
|
-
|
|
50
|
-
// 컬럼 삭제
|
|
51
|
-
await db.dropColumn({ name: "user" }, "phone");
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## 키/인덱스 DDL
|
|
55
|
-
|
|
56
|
-
```typescript
|
|
57
|
-
// PK
|
|
58
|
-
await db.addPrimaryKey({ name: "user" }, ["id"]);
|
|
59
|
-
await db.dropPrimaryKey({ name: "user" });
|
|
60
|
-
|
|
61
|
-
// FK
|
|
62
|
-
await db.addForeignKey({ name: "order" }, "user", userRelationDef);
|
|
63
|
-
await db.dropForeignKey({ name: "order" }, "user");
|
|
64
|
-
|
|
65
|
-
// 인덱스
|
|
66
|
-
await db.addIndex({ name: "user" }, indexBuilder);
|
|
67
|
-
await db.dropIndex({ name: "user" }, ["email"]);
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## 뷰/프로시저 DDL
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
await db.createView(UserSummary);
|
|
74
|
-
await db.dropView({ name: "user_summary" });
|
|
75
|
-
|
|
76
|
-
await db.createProc(GetUserOrders);
|
|
77
|
-
await db.dropProc({ name: "get_user_orders" });
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## 스키마 관리
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
// 스키마 존재 여부
|
|
84
|
-
const exists = await db.schemaExists("mydb", "dbo");
|
|
85
|
-
|
|
86
|
-
// 스키마 내 모든 테이블 삭제
|
|
87
|
-
await db.clearSchema({ database: "mydb", schema: "dbo" });
|
|
88
|
-
|
|
89
|
-
// 테이블 TRUNCATE
|
|
90
|
-
await db.truncate({ name: "user" });
|
|
91
|
-
|
|
92
|
-
// FK 제약조건 토글
|
|
93
|
-
await db.switchFk({ name: "user" }, false); // FK 비활성화
|
|
94
|
-
// ... 벌크 작업 ...
|
|
95
|
-
await db.switchFk({ name: "user" }, true); // FK 활성화
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## 연결 & 트랜잭션
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
// 자동 트랜잭션 (connect -> begin -> callback -> commit/rollback -> close)
|
|
102
|
-
const result = await db.connect(async () => {
|
|
103
|
-
await db.user().insert([{ name: "Alice", createdAt: new DateTime() }]);
|
|
104
|
-
return await db.user().execute();
|
|
105
|
-
}, "SERIALIZABLE"); // 격리 수준 선택
|
|
106
|
-
|
|
107
|
-
// 트랜잭션 없이 연결 (DDL 작업 등)
|
|
108
|
-
await db.connectWithoutTransaction(async () => {
|
|
109
|
-
await db.initialize();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// 수동 트랜잭션 (connectWithoutTransaction 내에서)
|
|
113
|
-
await db.connectWithoutTransaction(async () => {
|
|
114
|
-
// DDL 먼저 실행 (트랜잭션 밖)
|
|
115
|
-
await db.createTable(NewTable);
|
|
116
|
-
|
|
117
|
-
// 이후 트랜잭션 내에서 DML 실행
|
|
118
|
-
await db.transaction(async () => {
|
|
119
|
-
await db.user().insert([{ name: "Bob", createdAt: new DateTime() }]);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
격리 수준: `"READ_UNCOMMITTED"`, `"READ_COMMITTED"`, `"REPEATABLE_READ"`, `"SERIALIZABLE"`
|
|
125
|
-
|
|
126
|
-
## DbContextExecutor 인터페이스
|
|
127
|
-
|
|
128
|
-
Node.js(`@simplysm/orm-node`)나 서비스 클라이언트(`@simplysm/service-client`)에서 구현.
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
interface DbContextExecutor {
|
|
132
|
-
connect(): Promise<void>;
|
|
133
|
-
close(): Promise<void>;
|
|
134
|
-
beginTransaction(isolationLevel?: IsolationLevel): Promise<void>;
|
|
135
|
-
commitTransaction(): Promise<void>;
|
|
136
|
-
rollbackTransaction(): Promise<void>;
|
|
137
|
-
executeDefs<T>(defs: QueryDef[], resultMetas?: (ResultMeta | undefined)[]): Promise<T[][]>;
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
## 마이그레이션
|
|
142
|
-
|
|
143
|
-
`defineDbContext`의 `migrations` 옵션으로 스키마 변경사항을 관리한다. `initialize()` 호출 시 아직 실행되지 않은 마이그레이션만 실행된다.
|
|
144
|
-
|
|
145
|
-
```typescript
|
|
146
|
-
const MyDb = defineDbContext({
|
|
147
|
-
tables: { user: User },
|
|
148
|
-
migrations: [
|
|
149
|
-
{
|
|
150
|
-
name: "20260105_001_create_user_table",
|
|
151
|
-
up: async (db) => {
|
|
152
|
-
await db.createTable(User);
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
name: "20260106_001_add_email_column",
|
|
157
|
-
up: async (db) => {
|
|
158
|
-
const c = createColumnFactory();
|
|
159
|
-
await db.addColumn({ name: "user" }, "email", c.varchar(200).nullable());
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
],
|
|
163
|
-
});
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
실행된 마이그레이션은 `_migration` 테이블에 기록된다.
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
// 마이그레이션 코드 조회
|
|
170
|
-
const migrations = await db._migration().execute();
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## DbTransactionError
|
|
174
|
-
|
|
175
|
-
트랜잭션 관련 에러를 DBMS 독립적으로 처리하기 위한 에러 클래스.
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
import { DbTransactionError, DbErrorCode } from "@simplysm/orm-common";
|
|
179
|
-
|
|
180
|
-
// DbErrorCode:
|
|
181
|
-
// - NO_ACTIVE_TRANSACTION: 활성 트랜잭션 없음
|
|
182
|
-
// - TRANSACTION_ALREADY_STARTED: 트랜잭션 이미 시작됨
|
|
183
|
-
// - DEADLOCK: 데드락 발생
|
|
184
|
-
// - LOCK_TIMEOUT: 잠금 타임아웃
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
## QueryDef 생성기
|
|
188
|
-
|
|
189
|
-
DDL 메서드 외에도 `get*QueryDef()` 메서드로 QueryDef만 생성할 수 있다 (직접 실행하지 않음).
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
const def = db.getCreateTableQueryDef(User);
|
|
193
|
-
const def2 = db.getAddColumnQueryDef({ name: "user" }, "phone", c.varchar(20));
|
|
194
|
-
// ... 등. 모든 DDL 메서드에 대응하는 get*QueryDef() 메서드가 있다.
|
|
195
|
-
```
|
package/docs/query.md
DELETED
|
@@ -1,469 +0,0 @@
|
|
|
1
|
-
# 쿼리 & 표현식
|
|
2
|
-
|
|
3
|
-
## Queryable -- SELECT 쿼리 빌더
|
|
4
|
-
|
|
5
|
-
### 기본 조회
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// 전체 조회
|
|
9
|
-
const users = await db.user().execute();
|
|
10
|
-
|
|
11
|
-
// 단건 조회 (2건 이상이면 throw)
|
|
12
|
-
const user = await db.user()
|
|
13
|
-
.where((c) => [expr.eq(c.id, 1)])
|
|
14
|
-
.single();
|
|
15
|
-
|
|
16
|
-
// 첫 번째 결과 (여러 건이어도 첫 번째만 반환)
|
|
17
|
-
const latest = await db.user()
|
|
18
|
-
.orderBy((c) => c.createdAt, "DESC")
|
|
19
|
-
.first();
|
|
20
|
-
|
|
21
|
-
// 행 수
|
|
22
|
-
const count = await db.user()
|
|
23
|
-
.where((c) => [expr.eq(c.isActive, true)])
|
|
24
|
-
.count();
|
|
25
|
-
|
|
26
|
-
// 존재 여부
|
|
27
|
-
const hasAdmin = await db.user()
|
|
28
|
-
.where((c) => [expr.eq(c.role, "admin")])
|
|
29
|
-
.exists();
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### 필터링 (WHERE)
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
db.user()
|
|
36
|
-
.where((c) => [
|
|
37
|
-
expr.eq(c.name, "Alice"), // name = 'Alice'
|
|
38
|
-
expr.gt(c.score, 80), // score > 80
|
|
39
|
-
expr.between(c.createdAt, from, to), // BETWEEN
|
|
40
|
-
expr.like(c.email, "%@gmail.com"), // LIKE
|
|
41
|
-
expr.in(c.role, ["admin", "user"]), // IN
|
|
42
|
-
expr.null(c.deletedAt), // IS NULL
|
|
43
|
-
])
|
|
44
|
-
// 배열은 자동 AND 결합. OR는 명시적으로:
|
|
45
|
-
.where((c) => [
|
|
46
|
-
expr.or([
|
|
47
|
-
expr.eq(c.role, "admin"),
|
|
48
|
-
expr.gt(c.score, 90),
|
|
49
|
-
]),
|
|
50
|
-
])
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
where()를 여러 번 호출하면 AND로 결합된다.
|
|
54
|
-
|
|
55
|
-
### 텍스트 검색 (search)
|
|
56
|
-
|
|
57
|
-
```typescript
|
|
58
|
-
// 여러 컬럼에서 텍스트 검색
|
|
59
|
-
db.user()
|
|
60
|
-
.search((c) => [c.name, c.email], "John +admin -withdrawn")
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
검색 문법: `term`(OR), `+term`(AND 필수), `-term`(NOT 제외), `"exact"`(정확히), `wild*`(접두사). 내부적으로 `parseSearchQuery()`를 사용하여 SQL LIKE 패턴으로 변환한다.
|
|
64
|
-
|
|
65
|
-
### 정렬 (ORDER BY)
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
// orderBy는 컬럼 하나씩, 여러 번 호출 가능
|
|
69
|
-
db.user()
|
|
70
|
-
.orderBy((c) => c.createdAt, "DESC")
|
|
71
|
-
.orderBy((c) => c.name) // 기본: ASC
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### 페이징
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
// top: ORDER BY 없이도 사용 가능
|
|
78
|
-
db.user().top(10)
|
|
79
|
-
|
|
80
|
-
// limit: ORDER BY 필수 (skip, take)
|
|
81
|
-
db.user()
|
|
82
|
-
.orderBy((c) => c.createdAt, "DESC")
|
|
83
|
-
.limit(20, 10) // 20건 건너뛰고 10건 가져오기
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### 컬럼 선택 (SELECT)
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
db.user()
|
|
90
|
-
.select((c) => ({
|
|
91
|
-
id: c.id,
|
|
92
|
-
upperName: expr.upper(c.name),
|
|
93
|
-
emailDomain: expr.right(c.email, 10),
|
|
94
|
-
}))
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### 그룹화 (GROUP BY)
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
db.order()
|
|
101
|
-
.select((c) => ({
|
|
102
|
-
userId: c.userId,
|
|
103
|
-
total: expr.sum(c.amount),
|
|
104
|
-
count: expr.count(),
|
|
105
|
-
avg: expr.avg(c.amount),
|
|
106
|
-
}))
|
|
107
|
-
.groupBy((c) => [c.userId])
|
|
108
|
-
.having((c) => [expr.gt(expr.count(), 5)])
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### JOIN
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
// LEFT JOIN (1:N, 배열로 결과에 추가)
|
|
115
|
-
db.user()
|
|
116
|
-
.join("posts", (qr, u) =>
|
|
117
|
-
qr.from(Post)
|
|
118
|
-
.where((p) => [expr.eq(p.userId, u.id)])
|
|
119
|
-
)
|
|
120
|
-
// 결과: { id, name, posts: [{ id, title }, ...] }
|
|
121
|
-
|
|
122
|
-
// LEFT JOIN SINGLE (N:1, 단일 객체로 결과에 추가)
|
|
123
|
-
db.order()
|
|
124
|
-
.joinSingle("user", (qr, o) =>
|
|
125
|
-
qr.from(User)
|
|
126
|
-
.where((u) => [expr.eq(u.id, o.userId)])
|
|
127
|
-
)
|
|
128
|
-
// 결과: { id, amount, user: { id, name } | undefined }
|
|
129
|
-
|
|
130
|
-
// 관계 자동 로드 (include) -- 정의된 관계 기반 자동 JOIN
|
|
131
|
-
db.order()
|
|
132
|
-
.include((c) => c.user) // N:1 관계 로드
|
|
133
|
-
.include((c) => c.user.company) // 중첩 관계
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### 서브쿼리
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
// 스칼라 서브쿼리
|
|
140
|
-
db.user().select((c) => ({
|
|
141
|
-
name: c.name,
|
|
142
|
-
orderCount: expr.subquery("number",
|
|
143
|
-
db.order()
|
|
144
|
-
.where((o) => [expr.eq(o.userId, c.id)])
|
|
145
|
-
.select(() => ({ cnt: expr.count() }))
|
|
146
|
-
),
|
|
147
|
-
}))
|
|
148
|
-
|
|
149
|
-
// IN 서브쿼리
|
|
150
|
-
db.user().where((c) => [
|
|
151
|
-
expr.inQuery(c.id,
|
|
152
|
-
db.order()
|
|
153
|
-
.where((o) => [expr.gt(o.amount, 1000)])
|
|
154
|
-
.select((o) => ({ userId: o.userId }))
|
|
155
|
-
),
|
|
156
|
-
])
|
|
157
|
-
|
|
158
|
-
// EXISTS
|
|
159
|
-
db.user().where((c) => [
|
|
160
|
-
expr.exists(db.order().where((o) => [expr.eq(o.userId, c.id)])),
|
|
161
|
-
])
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### 잠금 (FOR UPDATE)
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
await db.connect(async () => {
|
|
168
|
-
const user = await db.user()
|
|
169
|
-
.where((c) => [expr.eq(c.id, 1)])
|
|
170
|
-
.lock()
|
|
171
|
-
.single();
|
|
172
|
-
});
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### DISTINCT
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
db.user().select((c) => ({ role: c.role })).distinct()
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### 서브쿼리 래핑 (wrap)
|
|
182
|
-
|
|
183
|
-
DISTINCT나 GROUP BY 이후 count() 등을 사용하려면 wrap()으로 서브쿼리화해야 한다.
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
const count = await db.user()
|
|
187
|
-
.select((c) => ({ name: c.name }))
|
|
188
|
-
.distinct()
|
|
189
|
-
.wrap()
|
|
190
|
-
.count();
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### UNION
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
const combined = Queryable.union(
|
|
197
|
-
db.user().where((c) => [expr.eq(c.type, "admin")]),
|
|
198
|
-
db.user().where((c) => [expr.eq(c.type, "manager")]),
|
|
199
|
-
);
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### 재귀 쿼리 (CTE)
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
// 계층 데이터 조회 (조직도, 카테고리 트리 등)
|
|
206
|
-
db.employee()
|
|
207
|
-
.where((e) => [expr.null(e.managerId)]) // 루트 노드
|
|
208
|
-
.recursive((cte) =>
|
|
209
|
-
cte.from(Employee)
|
|
210
|
-
.where((e) => [expr.eq(e.managerId, e.self[0].id)])
|
|
211
|
-
)
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
---
|
|
215
|
-
|
|
216
|
-
## CUD 연산
|
|
217
|
-
|
|
218
|
-
`select()`, `groupBy()`, `join()` 이후에는 CUD 불가 (타입 레벨에서 차단).
|
|
219
|
-
|
|
220
|
-
### INSERT
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
// 기본 삽입
|
|
224
|
-
await db.user().insert([
|
|
225
|
-
{ name: "Alice", email: "alice@example.com", createdAt: new DateTime() },
|
|
226
|
-
{ name: "Bob", createdAt: new DateTime() },
|
|
227
|
-
]);
|
|
228
|
-
|
|
229
|
-
// INSERT 후 컬럼 반환 (OUTPUT)
|
|
230
|
-
const [inserted] = await db.user().insert(
|
|
231
|
-
[{ name: "Alice", createdAt: new DateTime() }],
|
|
232
|
-
["id"], // 반환받을 컬럼
|
|
233
|
-
);
|
|
234
|
-
// inserted.id -> 자동 생성된 ID
|
|
235
|
-
|
|
236
|
-
// 조건부 INSERT (WHERE 조건에 맞는 데이터가 없을 때만)
|
|
237
|
-
await db.user()
|
|
238
|
-
.where((c) => [expr.eq(c.email, "test@test.com")])
|
|
239
|
-
.insertIfNotExists({ name: "testing", email: "test@test.com", createdAt: new DateTime() });
|
|
240
|
-
|
|
241
|
-
// INSERT INTO ... SELECT
|
|
242
|
-
await db.user()
|
|
243
|
-
.select((c) => ({ name: c.name, createdAt: c.createdAt }))
|
|
244
|
-
.where((c) => [expr.eq(c.isArchived, false)])
|
|
245
|
-
.insertInto(ArchivedUser);
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
### UPDATE
|
|
249
|
-
|
|
250
|
-
```typescript
|
|
251
|
-
await db.user()
|
|
252
|
-
.where((c) => [expr.eq(c.id, 1)])
|
|
253
|
-
.update((c) => ({ name: expr.val("string", "Alice2") }));
|
|
254
|
-
|
|
255
|
-
// OUTPUT으로 변경된 데이터 반환
|
|
256
|
-
const updated = await db.user()
|
|
257
|
-
.where((c) => [expr.eq(c.id, 1)])
|
|
258
|
-
.update(
|
|
259
|
-
(c) => ({ name: expr.val("string", "Alice2") }),
|
|
260
|
-
["id", "name"],
|
|
261
|
-
);
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### DELETE
|
|
265
|
-
|
|
266
|
-
```typescript
|
|
267
|
-
await db.user()
|
|
268
|
-
.where((c) => [expr.eq(c.id, 1)])
|
|
269
|
-
.delete();
|
|
270
|
-
|
|
271
|
-
// OUTPUT으로 삭제된 데이터 반환
|
|
272
|
-
const deleted = await db.user()
|
|
273
|
-
.where((c) => [expr.eq(c.isExpired, true)])
|
|
274
|
-
.delete(["id", "name"]);
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### UPSERT (UPDATE or INSERT)
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
// 동일 데이터로 UPDATE/INSERT
|
|
281
|
-
await db.user()
|
|
282
|
-
.where((c) => [expr.eq(c.email, "test@test.com")])
|
|
283
|
-
.upsert(() => ({
|
|
284
|
-
name: expr.val("string", "testing"),
|
|
285
|
-
email: expr.val("string", "test@test.com"),
|
|
286
|
-
}));
|
|
287
|
-
|
|
288
|
-
// UPDATE/INSERT 데이터가 다른 경우
|
|
289
|
-
await db.user()
|
|
290
|
-
.where((c) => [expr.eq(c.email, "test@test.com")])
|
|
291
|
-
.upsert(
|
|
292
|
-
() => ({ loginCount: expr.val("number", 1) }), // UPDATE용
|
|
293
|
-
(update) => ({ ...update, email: expr.val("string", "test@test.com") }), // INSERT용
|
|
294
|
-
);
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
### FK 토글
|
|
298
|
-
|
|
299
|
-
```typescript
|
|
300
|
-
// Queryable에서 직접 FK on/off
|
|
301
|
-
await db.user().switchFk(false); // FK 비활성화
|
|
302
|
-
// ... 벌크 작업 ...
|
|
303
|
-
await db.user().switchFk(true); // FK 활성화
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## Executable -- 프로시저 실행
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
const MyDb = defineDbContext({
|
|
312
|
-
tables: { user: User },
|
|
313
|
-
procedures: { getUserOrders: GetUserOrders },
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
const db = createDbContext(MyDb, executor, { database: "mydb" });
|
|
317
|
-
|
|
318
|
-
await db.connect(async () => {
|
|
319
|
-
const results = await db.getUserOrders().execute({ userId: 1 });
|
|
320
|
-
});
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
---
|
|
324
|
-
|
|
325
|
-
## expr -- 표현식 빌더
|
|
326
|
-
|
|
327
|
-
### 값/조건
|
|
328
|
-
|
|
329
|
-
| 함수 | 설명 |
|
|
330
|
-
|------|------|
|
|
331
|
-
| `expr.val(type, value)` | 리터럴 값 |
|
|
332
|
-
| `expr.raw(type)\`sql\`` | Raw SQL (태그 템플릿) |
|
|
333
|
-
| `expr.eq(a, b)` | `=` (NULL 안전) |
|
|
334
|
-
| `expr.gt(a, b)` | `>` |
|
|
335
|
-
| `expr.lt(a, b)` | `<` |
|
|
336
|
-
| `expr.gte(a, b)` | `>=` |
|
|
337
|
-
| `expr.lte(a, b)` | `<=` |
|
|
338
|
-
| `expr.between(src, from?, to?)` | BETWEEN |
|
|
339
|
-
| `expr.null(src)` | IS NULL |
|
|
340
|
-
| `expr.like(src, pattern)` | LIKE |
|
|
341
|
-
| `expr.regexp(src, pattern)` | 정규식 매칭 |
|
|
342
|
-
| `expr.in(src, values)` | IN (값 목록) |
|
|
343
|
-
| `expr.inQuery(src, query)` | IN (서브쿼리) |
|
|
344
|
-
| `expr.exists(query)` | EXISTS |
|
|
345
|
-
| `expr.not(cond)` | NOT |
|
|
346
|
-
| `expr.and(conds)` | AND |
|
|
347
|
-
| `expr.or(conds)` | OR |
|
|
348
|
-
|
|
349
|
-
### 문자열 함수
|
|
350
|
-
|
|
351
|
-
| 함수 | 설명 |
|
|
352
|
-
|------|------|
|
|
353
|
-
| `expr.concat(...args)` | 연결 (NULL=빈문자열) |
|
|
354
|
-
| `expr.left(src, len)` | 왼쪽 N자 |
|
|
355
|
-
| `expr.right(src, len)` | 오른쪽 N자 |
|
|
356
|
-
| `expr.substring(src, start, len?)` | 부분 문자열 (1-based) |
|
|
357
|
-
| `expr.trim(src)` | 양쪽 공백 제거 |
|
|
358
|
-
| `expr.padStart(src, len, fill)` | 왼쪽 패딩 (LPAD) |
|
|
359
|
-
| `expr.replace(src, from, to)` | 치환 |
|
|
360
|
-
| `expr.upper(src)` / `expr.lower(src)` | 대/소문자 |
|
|
361
|
-
| `expr.length(src)` | 문자 길이 (CHAR_LENGTH) |
|
|
362
|
-
| `expr.byteLength(src)` | 바이트 길이 (LENGTH/DATALENGTH) |
|
|
363
|
-
| `expr.indexOf(src, search)` | 위치 (1-based, 0=없음) |
|
|
364
|
-
|
|
365
|
-
### 수학 함수
|
|
366
|
-
|
|
367
|
-
| 함수 | 설명 |
|
|
368
|
-
|------|------|
|
|
369
|
-
| `expr.abs(src)` | 절대값 |
|
|
370
|
-
| `expr.round(src, digits)` | 반올림 |
|
|
371
|
-
| `expr.ceil(src)` / `expr.floor(src)` | 올림/내림 |
|
|
372
|
-
|
|
373
|
-
### 날짜 함수
|
|
374
|
-
|
|
375
|
-
| 함수 | 설명 |
|
|
376
|
-
|------|------|
|
|
377
|
-
| `expr.year(src)` / `expr.month(src)` / `expr.day(src)` | 연/월/일 추출 |
|
|
378
|
-
| `expr.hour(src)` / `expr.minute(src)` / `expr.second(src)` | 시/분/초 추출 |
|
|
379
|
-
| `expr.dateDiff(unit, from, to)` | 날짜 차이 |
|
|
380
|
-
| `expr.dateAdd(unit, src, value)` | 날짜 더하기 |
|
|
381
|
-
| `expr.formatDate(src, format)` | 날짜 포맷 (FORMAT/DATE_FORMAT) |
|
|
382
|
-
| `expr.isoWeek(src)` | ISO 주차 |
|
|
383
|
-
| `expr.isoWeekStartDate(src)` | ISO 주 시작일 |
|
|
384
|
-
| `expr.isoYearMonth(src)` | ISO 연월 (YYYYMM 형식) |
|
|
385
|
-
|
|
386
|
-
단위(`DateUnit`): `"year"`, `"month"`, `"day"`, `"hour"`, `"minute"`, `"second"`
|
|
387
|
-
|
|
388
|
-
### 집계 함수
|
|
389
|
-
|
|
390
|
-
| 함수 | 설명 |
|
|
391
|
-
|------|------|
|
|
392
|
-
| `expr.count(arg?, distinct?)` | 행 수 |
|
|
393
|
-
| `expr.sum(arg)` | 합계 |
|
|
394
|
-
| `expr.avg(arg)` | 평균 |
|
|
395
|
-
| `expr.max(arg)` / `expr.min(arg)` | 최대/최소 |
|
|
396
|
-
|
|
397
|
-
### 조건 함수
|
|
398
|
-
|
|
399
|
-
```typescript
|
|
400
|
-
// CASE WHEN
|
|
401
|
-
expr.switch<string>()
|
|
402
|
-
.case(expr.gt(c.score, 90), "A")
|
|
403
|
-
.case(expr.gt(c.score, 80), "B")
|
|
404
|
-
.default("C")
|
|
405
|
-
|
|
406
|
-
// IF (단순 삼항)
|
|
407
|
-
expr.if(expr.gt(c.score, 60), "pass", "fail")
|
|
408
|
-
|
|
409
|
-
// WHERE -> boolean 컬럼
|
|
410
|
-
expr.is(expr.gt(c.score, 80)) // true/false
|
|
411
|
-
|
|
412
|
-
// NULLIF (source === value이면 NULL 반환)
|
|
413
|
-
expr.nullIf(c.status, "unknown")
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
### 윈도우 함수
|
|
417
|
-
|
|
418
|
-
```typescript
|
|
419
|
-
// 순위
|
|
420
|
-
expr.rowNumber({ partitionBy: [c.dept], orderBy: [[c.score, "DESC"]] })
|
|
421
|
-
expr.rank({ orderBy: [[c.score, "DESC"]] })
|
|
422
|
-
expr.denseRank({ orderBy: [[c.score, "DESC"]] })
|
|
423
|
-
expr.ntile(4, { orderBy: [[c.score, "DESC"]] })
|
|
424
|
-
|
|
425
|
-
// 탐색
|
|
426
|
-
expr.lag(c.score, 1, 0, { orderBy: [[c.createdAt, "ASC"]] })
|
|
427
|
-
expr.lead(c.score, 1, undefined, { orderBy: [[c.createdAt, "ASC"]] })
|
|
428
|
-
expr.firstValue(c.score, { orderBy: [[c.createdAt, "ASC"]] })
|
|
429
|
-
expr.lastValue(c.score, { orderBy: [[c.createdAt, "ASC"]] })
|
|
430
|
-
|
|
431
|
-
// 윈도우 집계
|
|
432
|
-
expr.sumOver(c.amount, { partitionBy: [c.dept] })
|
|
433
|
-
expr.avgOver(c.amount, { partitionBy: [c.dept] })
|
|
434
|
-
expr.countOver({ partitionBy: [c.dept] })
|
|
435
|
-
expr.minOver(c.amount, { partitionBy: [c.dept] })
|
|
436
|
-
expr.maxOver(c.amount, { partitionBy: [c.dept] })
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
### 기타
|
|
440
|
-
|
|
441
|
-
| 함수 | 설명 |
|
|
442
|
-
|------|------|
|
|
443
|
-
| `expr.cast(src, targetType)` | 타입 변환 |
|
|
444
|
-
| `expr.greatest(...args)` | 최대값 |
|
|
445
|
-
| `expr.least(...args)` | 최소값 |
|
|
446
|
-
| `expr.rowNum()` | 행 번호 (전체) |
|
|
447
|
-
| `expr.random()` | 랜덤 0~1 |
|
|
448
|
-
| `expr.subquery(type, queryable)` | 스칼라 서브쿼리 |
|
|
449
|
-
|
|
450
|
-
---
|
|
451
|
-
|
|
452
|
-
## 검색 파서
|
|
453
|
-
|
|
454
|
-
사용자 검색 문법을 SQL LIKE 패턴으로 변환.
|
|
455
|
-
|
|
456
|
-
```typescript
|
|
457
|
-
import { parseSearchQuery } from "@simplysm/orm-common";
|
|
458
|
-
|
|
459
|
-
parseSearchQuery('apple +fruit -rotten "green apple" wild*');
|
|
460
|
-
// {
|
|
461
|
-
// or: ["%apple%", "wild%"],
|
|
462
|
-
// must: ["%green apple%", "%fruit%"],
|
|
463
|
-
// not: ["%rotten%"],
|
|
464
|
-
// }
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
문법: `term`(OR), `+term`(AND 필수), `-term`(NOT 제외), `"exact"`(정확히 + 필수), `wild*`(접두사)
|
|
468
|
-
|
|
469
|
-
이스케이프: `\\` (리터럴 `\`), `\*` (리터럴 `*`), `\%` (리터럴 `%`), `\"` (리터럴 `"`), `\+` (리터럴 `+`), `\-` (리터럴 `-`)
|
package/docs/schema.md
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
# 스키마 정의
|
|
2
|
-
|
|
3
|
-
## Table
|
|
4
|
-
|
|
5
|
-
불변 빌더 패턴으로 테이블을 정의한다. 모든 메서드는 새 인스턴스를 반환한다.
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { Table } from "@simplysm/orm-common";
|
|
9
|
-
|
|
10
|
-
const User = Table("user")
|
|
11
|
-
.description("사용자 테이블")
|
|
12
|
-
.database("mydb") // 선택
|
|
13
|
-
.schema("dbo") // MSSQL/PostgreSQL, 선택
|
|
14
|
-
.columns((c) => ({
|
|
15
|
-
id: c.int().autoIncrement(),
|
|
16
|
-
name: c.varchar(100),
|
|
17
|
-
email: c.varchar(200).nullable(),
|
|
18
|
-
role: c.varchar(20).default("user"),
|
|
19
|
-
bio: c.text().nullable(),
|
|
20
|
-
avatar: c.binary().nullable(),
|
|
21
|
-
score: c.decimal(10, 2).nullable(),
|
|
22
|
-
active: c.boolean().default(true),
|
|
23
|
-
birthday: c.date().nullable(),
|
|
24
|
-
loginTime: c.time().nullable(),
|
|
25
|
-
createdAt: c.datetime(),
|
|
26
|
-
externalId: c.uuid().nullable(),
|
|
27
|
-
}))
|
|
28
|
-
.primaryKey("id") // 복합 PK 지원: .primaryKey("col1", "col2")
|
|
29
|
-
.indexes((i) => [
|
|
30
|
-
i.index("email").unique(),
|
|
31
|
-
i.index("name", "role").orderBy("ASC", "DESC"),
|
|
32
|
-
i.index("createdAt").name("ix_created"),
|
|
33
|
-
])
|
|
34
|
-
.relations((r) => ({
|
|
35
|
-
orders: r.foreignKeyTarget(() => Order, "user"), // 1:N
|
|
36
|
-
profile: r.foreignKeyTarget(() => Profile, "user").single(), // 1:1
|
|
37
|
-
}));
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### 컬럼 타입
|
|
41
|
-
|
|
42
|
-
| 메서드 | SQL 타입 | TypeScript 타입 |
|
|
43
|
-
|--------|---------|----------------|
|
|
44
|
-
| `c.int()` | INT | `number` |
|
|
45
|
-
| `c.bigint()` | BIGINT | `number` |
|
|
46
|
-
| `c.float()` | FLOAT | `number` |
|
|
47
|
-
| `c.double()` | DOUBLE | `number` |
|
|
48
|
-
| `c.decimal(p, s?)` | DECIMAL(p,s) | `number` |
|
|
49
|
-
| `c.varchar(len)` | VARCHAR(len) | `string` |
|
|
50
|
-
| `c.char(len)` | CHAR(len) | `string` |
|
|
51
|
-
| `c.text()` | LONGTEXT/TEXT | `string` |
|
|
52
|
-
| `c.boolean()` | BIT/TINYINT(1) | `boolean` |
|
|
53
|
-
| `c.datetime()` | DATETIME | `DateTime` |
|
|
54
|
-
| `c.date()` | DATE | `DateOnly` |
|
|
55
|
-
| `c.time()` | TIME | `Time` |
|
|
56
|
-
| `c.binary()` | LONGBLOB/VARBINARY/BYTEA | `Bytes` |
|
|
57
|
-
| `c.uuid()` | UNIQUEIDENTIFIER/UUID | `Uuid` |
|
|
58
|
-
|
|
59
|
-
### 컬럼 수정자
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
c.int().autoIncrement() // 자동 증가 (INSERT 시 선택적)
|
|
63
|
-
c.varchar(100).nullable() // NULL 허용 (타입에 undefined 추가)
|
|
64
|
-
c.varchar(20).default("x") // 기본값 (INSERT 시 선택적)
|
|
65
|
-
c.varchar(100).description("설명")
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### 타입 추론
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
User.$inferSelect // { id: number; name: string; email: string | undefined; ... }
|
|
72
|
-
User.$inferInsert // { name: string; createdAt: DateTime; } & { id?: number; email?: string; ... }
|
|
73
|
-
User.$inferUpdate // { id?: number; name?: string; ... } (모든 필드 선택적)
|
|
74
|
-
User.$inferColumns // { id: number; name: string; email: string | undefined; ... } (관계 제외)
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## 관계 (Relation)
|
|
80
|
-
|
|
81
|
-
### foreignKey -- N:1 (DB FK 생성)
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
const Order = Table("order")
|
|
85
|
-
.columns((c) => ({
|
|
86
|
-
id: c.int().autoIncrement(),
|
|
87
|
-
userId: c.int(),
|
|
88
|
-
}))
|
|
89
|
-
.primaryKey("id")
|
|
90
|
-
.relations((r) => ({
|
|
91
|
-
user: r.foreignKey(["userId"], () => User), // Order.userId -> User.id
|
|
92
|
-
}));
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### foreignKeyTarget -- 1:N / 1:1 (역참조)
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
const User = Table("user")
|
|
99
|
-
.columns((c) => ({ id: c.int().autoIncrement(), name: c.varchar(100) }))
|
|
100
|
-
.primaryKey("id")
|
|
101
|
-
.relations((r) => ({
|
|
102
|
-
orders: r.foreignKeyTarget(() => Order, "user"), // 1:N (배열)
|
|
103
|
-
profile: r.foreignKeyTarget(() => Profile, "user").single(), // 1:1 (단일 객체)
|
|
104
|
-
}));
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### relationKey / relationKeyTarget -- 논리적 관계 (DB FK 없음)
|
|
108
|
-
|
|
109
|
-
View에서도 사용 가능. DB에 FK 제약조건을 생성하지 않는다.
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
.relations((r) => ({
|
|
113
|
-
category: r.relationKey(["categoryId"], () => Category),
|
|
114
|
-
items: r.relationKeyTarget(() => Item, "parent"),
|
|
115
|
-
}))
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## View
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
import { View, expr } from "@simplysm/orm-common";
|
|
124
|
-
|
|
125
|
-
const UserSummary = View("user_summary")
|
|
126
|
-
.database("mydb")
|
|
127
|
-
.query<typeof MyDb>((db) =>
|
|
128
|
-
db.user()
|
|
129
|
-
.select((c) => ({
|
|
130
|
-
userId: c.id,
|
|
131
|
-
userName: c.name,
|
|
132
|
-
orderCount: expr.count(),
|
|
133
|
-
totalAmount: expr.sum(c.amount),
|
|
134
|
-
}))
|
|
135
|
-
.groupBy((c) => [c.id, c.name])
|
|
136
|
-
)
|
|
137
|
-
.relations((r) => ({
|
|
138
|
-
user: r.relationKey(["userId"], () => User),
|
|
139
|
-
}));
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
**DbContext 등록:**
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
const MyDb = defineDbContext({
|
|
146
|
-
tables: { user: User },
|
|
147
|
-
views: { userSummary: UserSummary },
|
|
148
|
-
});
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## Procedure
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
import { Procedure } from "@simplysm/orm-common";
|
|
157
|
-
|
|
158
|
-
const GetUserOrders = Procedure("get_user_orders")
|
|
159
|
-
.database("mydb")
|
|
160
|
-
.params((c) => ({
|
|
161
|
-
userId: c.int(),
|
|
162
|
-
fromDate: c.date().nullable(),
|
|
163
|
-
}))
|
|
164
|
-
.returns((c) => ({
|
|
165
|
-
orderId: c.int(),
|
|
166
|
-
amount: c.decimal(10, 2),
|
|
167
|
-
createdAt: c.datetime(),
|
|
168
|
-
}))
|
|
169
|
-
.body(`
|
|
170
|
-
SELECT id AS orderId, amount, created_at AS createdAt
|
|
171
|
-
FROM orders
|
|
172
|
-
WHERE user_id = userId AND created_at >= COALESCE(fromDate, '1900-01-01')
|
|
173
|
-
`);
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
**DbContext 등록:**
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
const MyDb = defineDbContext({
|
|
180
|
-
tables: { user: User },
|
|
181
|
-
procedures: { getUserOrders: GetUserOrders },
|
|
182
|
-
});
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
MSSQL은 파라미터에 `@` 접두사 필요: `@userId`, `@fromDate`
|
|
186
|
-
|
|
187
|
-
---
|
|
188
|
-
|
|
189
|
-
## Index
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
.indexes((i) => [
|
|
193
|
-
i.index("email").unique(), // UNIQUE INDEX
|
|
194
|
-
i.index("name", "role").orderBy("ASC", "DESC"), // 정렬 방향 지정
|
|
195
|
-
i.index("createdAt").name("ix_custom_name"), // 커스텀 이름
|
|
196
|
-
i.index("code").description("코드 인덱스"),
|
|
197
|
-
])
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
|
|
202
|
-
## defineDbContext / createDbContext
|
|
203
|
-
|
|
204
|
-
### defineDbContext -- 스키마 블루프린트
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
|
-
import { defineDbContext } from "@simplysm/orm-common";
|
|
208
|
-
|
|
209
|
-
const MyDb = defineDbContext({
|
|
210
|
-
tables: { user: User, order: Order },
|
|
211
|
-
views: { userSummary: UserSummary },
|
|
212
|
-
procedures: { getUserOrders: GetUserOrders },
|
|
213
|
-
migrations: [
|
|
214
|
-
{
|
|
215
|
-
name: "20260105_001_add_phone",
|
|
216
|
-
up: async (db) => {
|
|
217
|
-
const c = createColumnFactory();
|
|
218
|
-
await db.addColumn({ name: "user" }, "phone", c.varchar(20).nullable());
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
],
|
|
222
|
-
});
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
`_migration` 테이블이 자동으로 포함된다.
|
|
226
|
-
|
|
227
|
-
### createDbContext -- 런타임 인스턴스 생성
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
import { createDbContext } from "@simplysm/orm-common";
|
|
231
|
-
|
|
232
|
-
const db = createDbContext(MyDb, executor, {
|
|
233
|
-
database: "mydb",
|
|
234
|
-
schema: "dbo", // MSSQL/PostgreSQL 선택
|
|
235
|
-
});
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
`executor`는 `DbContextExecutor` 인터페이스를 구현해야 한다 (`@simplysm/orm-node`의 `NodeDbContextExecutor` 등).
|