title: "스크립트로 SELECT · 트랜잭션" chapter: 6 images:
- script-select-cursor.png
스크립트로 SELECT · 트랜잭션
order_history 에서 데이터를 읽어 화면에 띄우거나, 여러 INSERT/UPDATE 를 한 단위로 묶는 단계입니다.
SELECT 두 가지 패턴(일괄 캐싱 과 커서 스트리밍) 을 처음부터 둘 다 익혀두면
이후 화면 표시(7장) 와 통계 처리에 그대로 적용할 수 있습니다.
public/manual/gui/script-select-cursor.png두 가지 SELECT 패턴
| 패턴 | 메서드 | 용도 |
|---|---|---|
| B · 일괄 캐싱 | RunSqlSelect + RowCount + GetValue(...) | 소량(수십~수백 행) · 화면 표시용 |
| A · 커서 스트리밍 | SelectBegin + while(SelectNext) + SelectEnd | 대량(수천 행 이상) · 집계/변환 용 |
용도가 다르니 처음부터 둘 다 가르치는 것을 권장합니다. 일괄 패턴은 화면 표시(7장) 의 핵심, 커서 패턴은 통계 SQL 이 아닌 스크립트 측 집계 가 필요할 때 씁니다.
패턴 B — 일괄 캐싱 (DataGrid 표시용)
7장의 DataGrid 가 그대로 사용할 함수입니다. 결과를 모듈 변수 DispData[] 에
콤마 구분 문자열 한 행씩 채워 넣습니다.
// 모듈 상단에 선언
array DispData[] = {",,,,,"};
FUNCTION RefreshList()
{
// ==========================================================================
// Module : 'ViewOrderHistory'
// Caption : 최근 50건을 DispData 에 채워 DataGrid 갱신
// Return : bool
// ==========================================================================
if( X.DB["local"].RunSqlSelect(
"SELECT * FROM order_history" +
" ORDER BY id DESC LIMIT 50") == false )
{
LogError($"RefreshList Error : {X.DB[\"local\"].LastError}");
return false;
}
int count = X.DB["local"].RowCount;
DispData.Clear();
string row;
for(i, 0, count-1)
{
// ★ DataGrid 한 행 = 콤마 구분 문자열
row = $"{X.DB[\"local\"].GetValueInt(i, \"id\"):D04}," +
$"{X.DB[\"local\"].GetValue(i, \"order_no\")}," +
$"{X.DB[\"local\"].GetValue(i, \"menu_name\")}," +
$"{X.DB[\"local\"].GetValue(i, \"start_time\")}," +
$"{X.DB[\"local\"].GetValueDouble(i, \"weight_g\"):F1}," +
$"{X.DB[\"local\"].GetValue(i, \"result\")}";
DispData.Add(row);
}
return true;
}셀 접근 메서드
| 메서드 | 반환 |
|---|---|
GetValue(row, colName) | string (가장 일반적) |
GetValueInt(row, colName) | int (실패 시 0) |
GetValueDouble(row, colName) | double (실패 시 0.0) |
IsNull(row, colName) | bool |
GetColumnArray(colName) | 컬럼 전체를 1차원 XArray 로 |
GetRowArray(row) | 행 전체를 1차원 XArray 로 |
행 수 / 컬럼 수는 RowCount · ColumnCount 속성.
패턴 A — 커서 스트리밍 (대량 집계)
대량 데이터를 읽으면서 메모리에 모두 쌓지 않고 한 행씩 처리합니다.
SelectEnd() 를 반드시 호출 해야 다음 쿼리가 실행됩니다.
FUNCTION CountTodayNg()
{
// ==========================================================================
// Caption : 오늘의 NG 건수 (스크립트 측 집계 예시)
// Return : int (실패 시 -1)
// ==========================================================================
string sql =
"SELECT result FROM order_history" +
$" WHERE start_time LIKE '{SYS.DateString}%'";
if( X.DB["local"].SelectBegin(sql) == false )
{
LogError($"SelectBegin Error : {X.DB[\"local\"].LastError}");
return -1;
}
int ng = 0;
while( X.DB["local"].SelectNext() )
{
if( X.DB["local"].GetString("result") == "NG" )
{
ng ++;
}
}
X.DB["local"].SelectEnd(); // 반드시 호출
Log($"CountTodayNg : ng={ng}");
return ng;
}커서 모드 메서드
| 메서드 | 설명 |
|---|---|
SelectBegin(sql) | 커서 시작 |
SelectBeginParam(sql, vals) | 파라미터 바인딩 버전 |
SelectNext() | 다음 행으로. 행 없으면 false |
GetString(colName) / GetInt / GetDouble / GetBool | 현재 행의 셀 |
SelectEnd() | 커서 종료 — 잊지 말 것 |
같은 일을 단일 SQL 로
이 예시는 학습용입니다. 실제 운영에서는 SQL 한 줄이 더 빠르고 단순합니다.
int ng = X.DB["local"].RunSqlScalarInt(
$"SELECT COUNT(*) FROM order_history" +
$" WHERE start_time LIKE '{SYS.DateString}%'" +
$" AND result='NG'");커서 패턴은 SQL 로 표현하기 어려운 집계 나 여러 컬럼을 동시에 변환/정제 할 때 진가가 드러납니다.
스칼라 SELECT — 통계 표시용
단일 셀(첫 행, 첫 컬럼) 만 필요하면 RunSqlScalar* 시리즈가 가장 단순합니다.
int todayCount;
int todayNg;
double todayAvg;
FUNCTION UpdateStats()
{
string today = SYS.DateString;
todayCount = X.DB["local"].RunSqlScalarInt(
$"SELECT COUNT(*) FROM order_history" +
$" WHERE start_time LIKE '{today}%'");
todayNg = X.DB["local"].RunSqlScalarInt(
$"SELECT COUNT(*) FROM order_history" +
$" WHERE start_time LIKE '{today}%' AND result='NG'");
todayAvg = X.DB["local"].RunSqlScalarDouble(
$"SELECT AVG(weight_g) FROM order_history" +
$" WHERE start_time LIKE '{today}%'");
}이 변수들은 7장에서 Label 컨트롤에 직접 연결합니다.
트랜잭션 — 여러 INSERT/UPDATE 를 한 단위로
여러 행을 한꺼번에 기록하다가 중간에 실패하면 부분 기록 이 남아 데이터가 망가집니다. 트랜잭션으로 감싸면 실패 시 모두 되돌립니다.
FUNCTION InsertOrderBatch(array orderNos, string menuName)
{
if( X.DB["local"].BeginTransaction() == false )
{
LogError($"BeginTransaction Error");
return false;
}
bool ok = true;
for(i, 0, orderNos.Count-1)
{
if( BeginOrder(/*orderNo*/orderNos[i], /*menuName*/menuName) == false )
{
ok = false;
break;
}
}
if( ok )
{
X.DB["local"].Commit();
Log($"InsertOrderBatch Commit : count={orderNos.Count}");
return true;
}
X.DB["local"].Rollback();
LogError($"InsertOrderBatch Rollback : {X.DB[\"local\"].LastError}");
return false;
}언제 트랜잭션을 쓰나
- 한 사이클의 다건 INSERT/UPDATE
- 두 테이블이 동시에 갱신되어야 할 때 (ex.
order_history+log) - 실패 시 ‘부분 기록’ 이 남으면 안 되는 케이스
단건 INSERT 는 굳이 트랜잭션을 걸지 않아도 됩니다.
주의
- 트랜잭션 도중
SelectBegin/SelectEnd의 커서 모드를 함께 쓰지 않습니다. 같은 연결에서 커서가 열려 있으면 INSERT 가 막힙니다. - 너무 많은 행을 한 트랜잭션에 넣지 않습니다. SQLite 잠금/메모리에 부담이 됩니다. 1만 건 단위 정도라면 1000건씩 나눠 트랜잭션을 분리하는 편이 안전합니다.
- 예외로 빠져나오는 흐름 이 있다면 finally 가 아니라 명시적인 분기 +
Rollback()으로 마무리합니다.
체크포인트
RefreshList()호출 후DispData.Count가 화면 행 수와 일치한다.CountTodayNg()가 SQL 탭 결과와 동일한 값을 반환한다.InsertOrderBatch의orderNos중간에 잘못된 값을 넣어 일부러 실패시키면 — 테이블에 1건도 추가되지 않아야 한다 (Rollback 검증).