Database 매뉴얼 · Chapter 6

title: "스크립트로 SELECT · 트랜잭션" chapter: 6 images:

  • script-select-cursor.png

스크립트로 SELECT · 트랜잭션

order_history 에서 데이터를 읽어 화면에 띄우거나, 여러 INSERT/UPDATE 를 한 단위로 묶는 단계입니다. SELECT 두 가지 패턴(일괄 캐싱커서 스트리밍) 을 처음부터 둘 다 익혀두면 이후 화면 표시(7장) 와 통계 처리에 그대로 적용할 수 있습니다.

일괄 캐싱과 커서 스트리밍 비교

두 가지 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() 으로 마무리합니다.

체크포인트

  1. RefreshList() 호출 후 DispData.Count 가 화면 행 수와 일치한다.
  2. CountTodayNg() 가 SQL 탭 결과와 동일한 값을 반환한다.
  3. InsertOrderBatchorderNos 중간에 잘못된 값을 넣어 일부러 실패시키면 — 테이블에 1건도 추가되지 않아야 한다 (Rollback 검증).

다음 단계

7장 — DataGrid 에 array 변수 연결