EternalWindows
ODBC / ステートメントの実行

ODBCドライバに接続したアプリケーションは、ドライバに対してSQL文を指定する前に、 ステートメントハンドルを取得することになります。 ステートメントハンドルは、ドライバによって作成されたSQL文の処理結果を格納し、 環境ハンドルなどと同じようにSQLAllocHandleで取得することができます。 これを取得すれば、SQLExecDirectでODBCドライバにSQL文を指定することができます。

SQLRETURN SQLExecDirect(
  SQLHSTMT StatementHandle,
  SQLCHAR * StatementText,
  SQLINTEGER TextLength
);

StatementHandleは、ステートメントハンドルを指定します。 StatementTextは、SQLの文法に従って整形された文字列を指定します。 TextLengthは、SQL_NTSを指定します。

SQLExecDirectが失敗する原因は、第2引数に指定した文字列の文法が不正である場合や、 存在しないテーブルを指定した場合など様々です。 ただし、次のような場合は、失敗として解釈されないので注意してください。

SQLRETURN nResult;
SQLLEN    nRowCount;
SQLTCHAR  szBuf[256];

nResult = SQLExecDirect(hstmt, (SQLTCHAR *)TEXT("DELETE FROM fruit WHERE price = 300"), SQL_NTS);
if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO)
	return FALSE;

SQLRowCount(hstmt, &nRowCount);
wsprintf(szBuf, TEXT("%d件のデータを削除しました。"), nRowCount);
MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);

このSQLExecDirectの第2引数に指定しているSQL文は、fruitというテーブルでpriceという列が300であるレコードを削除する意味を持ちます。 300であるデータが存在しない場合は、何らかのレコードが削除されることはありませんが、 このような場合でもSQLExecDirect自体は成功しています。 つまり、データが本当に削除されたかどうかは、 データを何件削除できたかで判定する必要があります。 SQLRowCountは、SQL文の実行対象となったレコードの数を第2引数に返すため、 この値が0より大きいかを確認すればよいでしょう。

SQLを実行するためには、まずは操作の対象となるテーブルを作成しなければなりません。 よって、今回は次のようなテーブルを作成するプログラムを開発します。

name price
apple 100
orange 200

このテーブルは果物に関する情報を格納しているので、テーブル名はfruitと命名することにします。 テーブルの各行はレコードと呼ばれ、今回のテーブルの1つのレコードは1つの果物に関する情報を表します。 nameという列は果物の名前を、priceという列は果物の値段を格納します。

今回のプログラムは、データベースに独自のテーブルを作成します。

#include <windows.h>
#include <sqlext.h>

BOOL ExecuteStatement(SQLHSTMT hstmt);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	SQLHENV     henv;
	SQLHDBC     hdbc;
	SQLHSTMT    hstmt;
	SQLRETURN   nResult;
	SQLSMALLINT nSize;
	TCHAR       szState[6];
	TCHAR       szErrorMsg[1024];
	SQLINTEGER  nErrorCode;

	nResult = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv);
	if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO)
		return 0;
	
	nResult = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
	if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO) {
		SQLFreeHandle(SQL_HANDLE_ENV, henv);
		return 0;
	}
	
	nResult = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
	if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO) {
		SQLFreeHandle(SQL_HANDLE_ENV, henv);
		return 0;
	}

	nResult = SQLConnect(hdbc, (SQLTCHAR *)TEXT("sample_dsn"), SQL_NTS, (SQLTCHAR *)TEXT(""), SQL_NTS, (SQLTCHAR *)TEXT(""), SQL_NTS);
	if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO) {
		SQLGetDiagRec(SQL_HANDLE_DBC, hdbc, 1, (SQLTCHAR *)szState, &nErrorCode, (SQLTCHAR *)szErrorMsg, sizeof(szErrorMsg) / sizeof(TCHAR), &nSize);
		MessageBox(NULL, szErrorMsg, NULL, MB_ICONWARNING);
		SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
		SQLFreeHandle(SQL_HANDLE_ENV, henv);
		return 0;
	}

	nResult = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
	if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO) {
		SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
		SQLFreeHandle(SQL_HANDLE_ENV, henv);
		return 0;
	}

	if (!ExecuteStatement(hstmt)) {
		SQLGetDiagRec(SQL_HANDLE_STMT, hstmt, 1, (SQLTCHAR *)szState, &nErrorCode, (SQLTCHAR *)szErrorMsg, sizeof(szErrorMsg) / sizeof(TCHAR), &nSize);
		MessageBox(NULL, szErrorMsg, NULL, MB_ICONWARNING);
	}
	else
		MessageBox(NULL, TEXT("終了します。"), TEXT("OK"), MB_OK);
	
	SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
	SQLDisconnect(hstmt);
	SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
	SQLFreeHandle(SQL_HANDLE_ENV, henv);

	return 0;
}

BOOL ExecuteStatement(SQLHSTMT hstmt)
{
	BOOL      bCreateTable = TRUE;
	SQLRETURN nResult;

	if (bCreateTable) {
		nResult = SQLExecDirect(hstmt, (SQLTCHAR *)TEXT("CREATE TABLE fruit (name varchar(20), price int)"), SQL_NTS);
		if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO)
			return FALSE;
		
		nResult = SQLExecDirect(hstmt, (SQLTCHAR *)TEXT("INSERT INTO fruit VALUES('apple', 100)"), SQL_NTS);
		if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO)
			return FALSE;
		
		nResult = SQLExecDirect(hstmt, (SQLTCHAR *)TEXT("INSERT INTO fruit VALUES('orange', 200)"), SQL_NTS);
		if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO)
			return FALSE;
	}
	else {
		nResult = SQLExecDirect(hstmt, (SQLTCHAR *)TEXT("DROP TABLE fruit"), SQL_NTS);
		if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO)
			return FALSE;
	}

	return TRUE;
}

SQLConnectによる接続が成功したら、SQLを実行するためのステートメントハンドルを取得します。 これは、SQLAllocHandleにSQL_HANDLE_STMTを指定することで可能です。 次に、ExecuteStatementという自作関数で、 テーブルの作成または削除を行います。 bCreateTableがTRUEの場合は、テーブルの作成とレコードの追加が行われます。

if (bCreateTable) {
	nResult = SQLExecDirect(hstmt, (SQLTCHAR *)TEXT("CREATE TABLE fruit (name varchar(20), price int)"), SQL_NTS);
	if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO)
		return FALSE;
	
	nResult = SQLExecDirect(hstmt, (SQLTCHAR *)TEXT("INSERT INTO fruit VALUES('apple', 100)"), SQL_NTS);
	if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO)
		return FALSE;
	
	nResult = SQLExecDirect(hstmt, (SQLTCHAR *)TEXT("INSERT INTO fruit VALUES('orange', 200)"), SQL_NTS);
	if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO)
		return FALSE;
}

1回目のSQLExecDirectでは、テーブルを作成するCREATE TABLE文を実行しています。 CREATE TABLEという文字列の後は、作成するテーブルの名前であり、 ここではfruitという名前を指定します。 テーブル名の後の括弧には、テーブルの列名と型を複数指定します。 1つ目の列の名前はnameで型はvarchar(20)、2つ目の列の名前はpriceで型はintです。 2回目と3回目のSQLExecDirectでは、INSERT INTO文でテーブルにデータを格納します。 INSERT INTOという文字列の後は、データの格納先とするテーブルの名前であり、 VALUESという括弧には格納するデータを指定します。 fruitというテーブルの列は2つであるため、指定するデータの数も2つになります。 SQLExecDirectが失敗した場合は、SQLGetDiagRecでエラーの原因を表す文字列を取得します。

トランザクションについて

トランザクションとは、一連の処理を1つの処理として見なし、データの一貫性を保つための仕組みです。 トランザクションを開始した場合、これ以降に行われる処理は直ちにテーブルに反映されることはなく、 コミットと呼ばれる操作を行うことで、始めてテーブルに処理内容が反映されることになります。 また、ロールバックと呼ばれる操作を行えば、これまで行ってきた処理をなかったことにすることができます。 トランザクションの機能を理解するために、次のコードを例にします。

BOOL ExecuteStatement(SQLHSTMT hstmt)
{
	SQLRETURN nResult;
	
	nResult = SQLExecDirect(hstmt, (SQLTCHAR *)TEXT("UPDATE fruit SET price = 300 WHERE name = 'apple'"), SQL_NTS);
	if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO)
		return FALSE;
	
	nResult = SQLExecDirect(hstmt, (SQLTCHAR *)TEXT("UPDATE fruit SET price = 400 WHERE name = 'orange'"), SQL_NTS);
	if (nResult != SQL_SUCCESS && nResult != SQL_SUCCESS_WITH_INFO)
		return FALSE;
	
	return TRUE;
}

このコードは、appleのpriceを300に設定し、orangeのpriceを400に設定するという処理ですが、 ここで1つ条件を課すことにしましょう。 内容は、全ての処理が成功した場合のみ、テーブルのデータを変更するというものです。 言い換えれば、最初の処理が成功しても、2回目の処理が失敗した場合は、 最初の処理の成功をなかったことにするというものです。 このような場合、次のようなコードでトランザクションを実現することになります。

SQLSetConnectOption(hdbc, SQL_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF);

if (!ExecuteStatement(hstmt)) {
	SQLGetDiagRec(SQL_HANDLE_STMT, hstmt, 1, (SQLTCHAR *)szState, &nErrorCode, (SQLTCHAR *)szErrorMsg, sizeof(szErrorMsg) / sizeof(TCHAR), &nSize);
	MessageBox(NULL, szErrorMsg, NULL, MB_ICONWARNING);
	SQLEndTran(SQL_HANDLE_DBC, hdbc, SQL_ROLLBACK);
}
else {
	MessageBox(NULL, TEXT("終了します。"), TEXT("OK"), MB_OK);
	SQLEndTran(SQL_HANDLE_DBC, hdbc, SQL_COMMIT);
}

SQLSetConnectOption(hdbc, SQL_AUTOCOMMIT, SQL_AUTOCOMMIT_ON);

SQLSetConnectOptionの第2引数にSQL_AUTOCOMMITを指定した場合、 第3引数にSQL_AUTOCOMMIT_OFFを指定することができます。 これにより、データの追加や変更が自動で反映されることがなくなり、 トランザクションが開始されることになります。 ExecuteStatementが失敗した場合は、これまでの処理をなかったことにしなければなりませんから、 SQLEndTranの第3引数にSQL_ROLLBACKを指定します。 一方、関数が成功した場合は、処理が実際に反映されるようにSQL_COMMITを指定することになります。 SQLSetConnectOptionにSQL_AUTOCOMMIT_ONを指定した場合は、トランザクションが終了し、 データの追加や変更は即座に自動で反映されることになります。



戻る