Database 매뉴얼 · Chapter 5

INSERT · UPDATE · DELETE

Data.xms 에서 변경 계열 SQL 을 다루는 함수 4 개를 차례대로 살펴봅니다.

함수역할패턴
DB_InsertSample신규 주문 1 건 INSERT파라미터 바인딩
DB_UpdateSelected선택 행 작업 완료 처리트랜잭션 + 파라미터 바인딩
DB_DeleteSelected선택 행 삭제파라미터 바인딩
DB_InsertInitialSamples빈 테이블에 5 건 일괄 INSERT트랜잭션 (다건)

핵심 두 가지를 먼저 정리합니다.

1) 파라미터 바인딩 — RunSqlQueryParam(sql, paramArray)

문자열 연결로 SQL 을 만드는 대신, ? 자리표시자에 값을 배열로 전달합니다. SQL 인젝션을 차단하고 따옴표 이스케이프 같은 자잘한 문제도 사라집니다.

array p[] = {""};
p.Clear();
p.Add(value1);
p.Add(value2);
// ... ? 개수와 p 의 원소 수가 정확히 같아야 함
 
string sql = "INSERT INTO ... VALUES(?, ?, ?, ?)";
DB["local"].RunSqlQueryParam(sql, p);

타입은 자동으로 변환됩니다 (int / double / string / bool).

2) 트랜잭션 — BeginTransaction / Commit / Rollback

여러 SQL 을 한 묶음으로 실행하거나, 실패 시 모든 변경을 되돌리고 싶을 때 씁니다. 중간에 실패가 발생하면 반드시 Rollback — 안 그러면 다음 트랜잭션이 영향을 받습니다.

if( DB["local"].BeginTransaction() == false ) return false;
 
if( DB["local"].RunSqlQueryParam(sql, p) == false )
{
   DB["local"].Rollback();
   return false;
}
 
if( DB["local"].Commit() == false ) return false;

DB_InsertSample — 단건 INSERT

Add 버튼이 호출하는 함수입니다. 메뉴는 7 개에서 순환하고, 주문번호는 OrderSeq 카운터로 자동 증가합니다.

FUNCTION DB_InsertSample()
{
   if( DB["local"].IsOpen == false )
   {
      ShowMessage(EB_Ok, "DB is not open. Press [Open] first.");
      return false;
   }
 
   array menuPool[] = {"Americano", "Latte", "Cappuccino", "Espresso",
                      "Mocha", "Green Tea", "Lemonade"};
   OrderSeq = OrderSeq + 1;
   int menuIdx = OrderSeq % 7;
 
   array p[] = {""};
   p.Clear();
   p.Add($"O{OrderSeq}");          // order_no
   p.Add(menuPool[menuIdx]);       // menu_name
   p.Add(SYS.DateTimeString);      // start_time
   p.Add("");                      // end_time (작업 진행 중이라 비움)
   p.Add(0);                       // weight_g
   p.Add("Pending");               // result
   p.Add(0);                       // is_error
 
   string sql = "INSERT INTO order_history(order_no, menu_name, start_time, end_time, weight_g, result, is_error) VALUES(?,?,?,?,?,?,?)";
   if( DB["local"].RunSqlQueryParam(sql, p) == false )
   {
      LogError($"DB_InsertSample failed : {DB["local"].LastError}");
      ShowMessage(EB_Ok, $"DB Insert failed : {DB["local"].LastError}");
      return false;
   }
 
   Log($"DB_InsertSample : O{OrderSeq} {menuPool[menuIdx]} inserted");
 
   return DB_Refresh();
}

핵심 포인트:

  • 항상 IsOpen 으로 가드 — 닫힌 연결에 명령을 보내면 LastError 만 쌓입니다.
  • 실패 시 LogError + ShowMessage 두 군데 모두 출력 — 로그(추적용) + UI(현장 알림용).
  • 마지막에 DB_Refresh() 호출로 DataGrid 재로드 (6 장에서 다룸).

DB_UpdateSelected — 트랜잭션 UPDATE

Update 버튼이 호출합니다. DataGrid 의 선택 행 PK 를 읽어 작업 완료(Done) 로 변경합니다.

FUNCTION DB_UpdateSelected()
{
   if( DB["local"].IsOpen == false )
   {
      ShowMessage(EB_Ok, "DB is not open. Press [Open] first.");
      return false;
   }
 
   if( SelectIndex < 0 || SelectIndex >= DB["local"].RowCount )
   {
      ShowMessage(EB_Ok, "Select a row first.");
      return false;
   }
 
   // DataGrid 선택 행에서 PK(id) 조회
   int targetId = DB["local"].GetValueInt(/*row*/SelectIndex, /*colName*/"id");
 
   if( DB["local"].BeginTransaction() == false )
   {
      LogError($"BeginTransaction failed : {DB["local"].LastError}");
      return false;
   }
 
   array p[] = {""};
   p.Clear();
   p.Add(SYS.DateTimeString);   // end_time
   p.Add(250);                  // weight_g (시연용 고정값)
   p.Add("Done");               // result
   p.Add(targetId);             // WHERE id = ?
 
   string sql = "UPDATE order_history SET end_time=?, weight_g=?, result=? WHERE id=?";
   if( DB["local"].RunSqlQueryParam(sql, p) == false )
   {
      DB["local"].Rollback();
      LogError($"DB_UpdateSelected failed : {DB["local"].LastError}");
      return false;
   }
 
   if( DB["local"].Commit() == false )
   {
      LogError($"Commit failed : {DB["local"].LastError}");
      return false;
   }
 
   Log($"DB_UpdateSelected : id={targetId} updated");
 
   return DB_Refresh();
}

SelectIndex 는 7 장에서 다루는 DataGrid 의 선택 행 인덱스입니다. GetValueInt(SelectIndex, "id") 패턴이 핵심 — 행 번호와 컬럼 이름으로 셀 값을 읽습니다.

DB_DeleteSelected — 단순 DELETE

FUNCTION DB_DeleteSelected()
{
   // (가드 두 줄 동일)
 
   int targetId = DB["local"].GetValueInt(/*row*/SelectIndex, /*colName*/"id");
 
   array p[] = {""};
   p.Clear();
   p.Add(targetId);
 
   string sql = "DELETE FROM order_history WHERE id=?";
   if( DB["local"].RunSqlQueryParam(sql, p) == false )
   {
      LogError($"DB_DeleteSelected failed : {DB["local"].LastError}");
      return false;
   }
 
   Log($"DB_DeleteSelected : id={targetId} deleted");
   SelectIndex = -1;
 
   return DB_Refresh();
}

SelectIndex = -1 로 되돌려 두는 이유 — 삭제 직후 같은 인덱스를 다시 쓰면 다른 행을 가리키게 되기 때문입니다.

DB_InsertInitialSamples — 트랜잭션으로 다건

빈 테이블 첫 Open 시 한 번만 호출됩니다. 5 건을 한 트랜잭션으로 묶어 일관성과 속도를 함께 잡는 패턴입니다.

if( DB["local"].BeginTransaction() == false ) return false;
 
string sql = "INSERT INTO order_history(...) VALUES(?,?,?,?,?,?,?)";
 
for( i, 0, 4 )
{
   array p[] = {""};
   p.Clear();
   p.Add(/* ... */);
 
   if( DB["local"].RunSqlQueryParam(sql, p) == false )
   {
      DB["local"].Rollback();
      return false;
   }
}
 
if( DB["local"].Commit() == false ) return false;

5 건 처리 시는 차이가 작아 보이지만, 수백 ~ 수천 건 의 이력 데이터를 일괄 적재하는 실제 양산 환경에서는 트랜잭션 vs 비트랜잭션의 속도 차가 매우 큽니다.

다음 챕터로

읽기(SELECT) 쪽이 남았습니다 — RunSqlSelectGetRowArray, 단일 값 RunSqlScalarInt.