저번 시간의 불편함을 어느 정도 해소해 보자.
1. BindParam/Col
요주의 함수.
원래 있던건 private
로 돌려서 내부에서 사용하기로 하고,
public
으로 새로 만들 어떤 자료형이든 받을 수 있게 함수를 쫙 작성하기로 하자.
새로 작성할 함수들은 얘네들이다.
public:
bool BindParam(int32 paramIndex, bool* value, SQLLEN* index);
bool BindParam(int32 paramIndex, float* value, SQLLEN* index);
bool BindParam(int32 paramIndex, double* value, SQLLEN* index);
bool BindParam(int32 paramIndex, int8* value, SQLLEN* index);
bool BindParam(int32 paramIndex, int16* value, SQLLEN* index);
bool BindParam(int32 paramIndex, int32* value, SQLLEN* index);
bool BindParam(int32 paramIndex, int64* value, SQLLEN* index);
bool BindParam(int32 paramIndex, TIMESTAMP_STRUCT* value, SQLLEN* index);
bool BindParam(int32 paramIndex, const WCHAR* str, SQLLEN* index);
bool BindParam(int32 paramIndex, const BYTE* bin, int32 size, SQLLEN* index);
bool BindCol(int32 columnIndex, bool* value, SQLLEN* index);
bool BindCol(int32 columnIndex, float* value, SQLLEN* index);
bool BindCol(int32 columnIndex, double* value, SQLLEN* index);
bool BindCol(int32 columnIndex, int8* value, SQLLEN* index);
bool BindCol(int32 columnIndex, int16* value, SQLLEN* index);
bool BindCol(int32 columnIndex, int32* value, SQLLEN* index);
bool BindCol(int32 columnIndex, int64* value, SQLLEN* index);
bool BindCol(int32 columnIndex, TIMESTAMP_STRUCT* value, SQLLEN* index);
bool BindCol(int32 columnIndex, WCHAR* str, int32 size, SQLLEN* index);
bool BindCol(int32 columnIndex, BYTE* bin, int32 size, SQLLEN* index);
말 그대로 노가다...
// DBConnection.h
enum
{
WVARCHAR_MAX = 4000,
BINARY_MAX = 8000
};
// ...
// DBConnection.cpp
// ...
bool DBConnection::BindParam(int32 paramIndex, int64* value, SQLLEN* index)
{
return BindParam(paramIndex, SQL_C_SBIGINT, SQL_BIGINT, size32(int64), value, index);
}
bool DBConnection::BindParam(int32 paramIndex, TIMESTAMP_STRUCT* value, SQLLEN* index)
{
return BindParam(paramIndex, SQL_C_TYPE_TIMESTAMP, SQL_TYPE_TIMESTAMP, size32(TIMESTAMP_STRUCT), value, index);
}
bool DBConnection::BindParam(int32 paramIndex, const WCHAR* str, SQLLEN* index)
{
SQLULEN size = static_cast<SQLULEN>((::wcslen(str) + 1) * 2);
*index = SQL_NTSL;
if (size > WVARCHAR_MAX)
return BindParam(paramIndex, SQL_C_WCHAR, SQL_WLONGVARCHAR, size, (SQLPOINTER)str, index);
else
return BindParam(paramIndex, SQL_C_WCHAR, SQL_WVARCHAR, size, (SQLPOINTER)str, index);
}
bool DBConnection::BindParam(int32 paramIndex, const BYTE* bin, int32 size, SQLLEN* index)
{
if (bin == nullptr)
{
*index = SQL_NULL_DATA;
size = 1;
}
else
*index = size;
if (size > BINARY_MAX)
return BindParam(paramIndex, SQL_C_BINARY, SQL_LONGVARBINARY, size, (BYTE*)bin, index);
else
return BindParam(paramIndex, SQL_C_BINARY, SQL_BINARY, size, (BYTE*)bin, index);
}
너무 많아서 몇 가지만 떼 와서 보기로 했다.
위의 두 함수처럼 서로 타입을 맞춰주는 게 계속 반복된다.
특히 WCHAR
를 받는 부분과 BYTE
배열을 받는 부분을 주목할 만하다.
SQL에서 WCHAR
의 크기가 WVARCHAR_MAX
(4,000)을 넘어가면 SQL_WLONGVARCHAR
로 취급한다.
BYTE배열은 크기가 BINARY_MAX
(8,000)을 넘어가면 SQL_BINARY
가 아닌 SQL_LONGVARBINARY
로 취급한다.
이런 부분은 확실히 조심해야 할 것 같다.
BindCol()은 아래와 같이 평이한 흐름으로 정리된다.
bool DBConnection::BindCol(int32 columnIndex, bool* value, SQLLEN* index)
{
return BindCol(columnIndex, SQL_C_TINYINT, size32(bool), value, index);
}
bool DBConnection::BindCol(int32 columnIndex, float* value, SQLLEN* index)
{
return BindCol(columnIndex, SQL_C_FLOAT, size32(float), value, index);
}
bool DBConnection::BindCol(int32 columnIndex, double* value, SQLLEN* index)
{
return BindCol(columnIndex, SQL_C_DOUBLE, size32(double), value, index);
}
2. 일단 테스트
문자열과 시간에 관한 기능이 생기기도 했고 잘 동작하나 궁금하니 테스트해 보자.
테이블 생성 시 해당 내용이 포함되도록 했다.
auto query = L" \
DROP TABLE IF EXISTS [dbo].[Gold]; \
CREATE TABLE [dbo].[Gold] \
( \
[id] INT NOT NULL PRIMARY KEY IDENTITY, \
[gold] INT NULL, \
[name] NVARCHAR(50) NULL, \
[createDate] DATETIME NULL \
);";
// 기존에 바인딩 된 정보 날림
dbConn->Unbind();
// 넘길 인자 바인딩
int32 gold = 100;
SQLLEN len = 0;
WCHAR name[100] = L"이름이름이름";
SQLLEN nameLen = 0;
TIMESTAMP_STRUCT ts = {};
ts.year = 2023;
ts.month = 8;
ts.day = 29;
SQLLEN tsLen = 0;
// 넘길 인자 바인딩
ASSERT_CRASH(dbConn->BindParam(1, &gold, &len));
ASSERT_CRASH(dbConn->BindParam(2, name, &nameLen));
ASSERT_CRASH(dbConn->BindParam(3, &ts, &tsLen));
// SQL 실행
ASSERT_CRASH(dbConn->Execute(L"INSERT INTO [dbo].[Gold]([gold], [name], [createDate]) VALUES(?, ?, ?)"));
GDBConnectionPool->Push(dbConn);
확실히 바인딩 과정이 편해졌다.
데이터를 읽는 것은 아래와 같이 가능하다.
// 기존에 바인딩 된 정보 날림
dbConn->Unbind();
int32 gold = 100;
SQLLEN len = 0;
// 넘길 인자 바인딩
ASSERT_CRASH(dbConn->BindParam(1, &gold, &len));
int32 outId = 0;
SQLLEN outIdLen = 0;
ASSERT_CRASH(dbConn->BindCol(1, &outId, &outIdLen));
int32 outGold = 0;
SQLLEN outGoldLen = 0;
ASSERT_CRASH(dbConn->BindCol(2, &outGold, &outGoldLen));
WCHAR outName[100];
SQLLEN outNameLen = 0;
ASSERT_CRASH(dbConn->BindCol(3, outName, len32(outName), &outNameLen));
TIMESTAMP_STRUCT outDate = {};
SQLLEN outDateLen = 0;
ASSERT_CRASH(dbConn->BindCol(4, &outDate, &outDateLen));
// SQL 실행
ASSERT_CRASH(dbConn->Execute(L"SELECT id, gold, name, createDate FROM [dbo].[Gold] WHERE gold = (?)"));
wcout.imbue(locale("kor"));
while (dbConn->Fetch())
{
wcout << "Id: " << outId << " Gold : " << outGold << " Name: " << outName << endl;
wcout << "Date : " << outDate.year << "/" << outDate.month << "/" << outDate.day << endl;
}
GDBConnectionPool->Push(dbConn);
뽑을 데이터를 바인딩하는 과정이 단순해졌다.
실행도 잘 되지만 역시 더 과정을 단순하게 만들 수 있을 것 같다.
3. DBBind 클래스 작성
DB를 전담해 줄 일꾼을 만들어 보자.
template<int32 ParamCount, int32 ColumnCount>
class DBBind
{
public:
DBBind(DBConnection& dbConnection, const WCHAR* query)
: _dbConnection(dbConnection), _query(query)
{
::memset(_paramIndex, 0, sizeof(_paramIndex));
::memset(_columnIndex, 0, sizeof(_columnIndex));
_paramFlag = 0;
_columnFlag = 0;
dbConnection.Unbind();
}
bool Validate()
{
}
bool Execute()
{
ASSERT_CRASH(Validate());
return _dbConnection.Execute(_query);
}
bool Fetch()
{
return _dbConnection.Fetch();
}
public:
template<typename T>
void BindParam(int32 idx, T& value)
{
_dbConnection.BindParam(idx + 1, &value, &_paramIndex[idx]);
_paramFlag |= (1LL << idx);
// 1LL : 1을 long long으로 생각하겠다
// |= : OR 비트 연산 하여 값을 _paramFlag에 저장
}
// ...
template<typename T>
void BindCol(int32 idx, T& value)
{
_dbConnection.BindCol(idx + 1, &value, &_columnIndex[idx]);
_columnFlag |= (1LL << idx);
}
// ...
protected:
DBConnection& _dbConnection;
const WCHAR* _query;
SQLLEN _paramIndex[ParamCount > 0 ? ParamCount : 1];
SQLLEN _columnIndex[ColumnCount > 0 ? ColumnCount : 1];
uint64 _paramFlag;
uint64 _columnFlag;
};
- Validate()
- 비트플래그 검증용 함수. 이후에 구현 - Execute()
- 실제로 쿼리문을 실행하는 함수 - Fetch()
- SELECT 등을 사용한 쿼리문의 결과를 가져오는 함수 - BindParam() / BindCol()
- 템플릿을 사용해 여러 가지 자료형을 받을 수 있다.
- 이 클래스를 활용해 코드 작성 시 0번부터 시작하는 느낌으로 사용할 수 있도록,idx
를 1 더해준다.
- 비트 플래그를 활용하기 때문에 비트연산자를 활용한다.
-1LL
을 사용한 이유는_paramFlag
의 자료형은uint64
이기 때문이다.
- 만약long long이라는 걸
알려주지 않는다면 문제가 생길 여지가 있다. - 변수
-_param / columnIndex
- 클래스 생성 시 받은 값에 따라 이들의 값도 정해진다.
- 입력 또는 출력이 몇 개인지 묻는 것이다.
-_param / columnFlag
-uint64
이기 때문에 최대 64개의 데이터를 사용할 수 있다.
Validate()의 경우는 이전에 시퀀스를 활용한 템플릿과 비슷한 형식으로 구현할 수 있다.
// 재귀적으로 감소하며 체크
template<int32 C>
struct FullBits { enum { value = (1 << (C - 1)) | FullBits<C-1>::value }; };
template<>
struct FullBits<1> { enum { value = 1 }; };
template<>
struct FullBits<0> { enum { value = 0 }; };
아래와 같은 순서로 체크가 진행된다.
FullBits<3>
가 있다고 가정- FullBits<3> = (1 << 2) | FullBits<2> 라는 형태로 볼 수 있다.
- Fullbits<2> = (1 << 1) | FullBits<1>의 형태가 되는 것이다.
- 궁극적으로 FullBIts<3> = (1 << 2) | (1 << 1) | (1 << 0)이 된다.
- 이
_paramFlag
와 비교해 제대로 비트가 채워져 있는지 검증할 수 있다.
따라서 Validate()의 구현은 다음과 같다.
bool Validate()
{
return _paramFlag == FullBits<ParamCount>::value && _columnFlag == FullBits<ColumnCount>::value;
}
4. 만들었으니 써봐야지
데이터를 꽂아 넣는 부분은 이렇게 구현할 수 있다.
// Add Data
for (int32 i = 0; i < 3; i++)
{
DBConnection* dbConn = GDBConnectionPool->Pop();
DBBind<3, 0> dbBind(*dbConn, L"INSERT INTO [dbo].[Gold]([gold], [name], [createDate]) VALUES(?, ?, ?)");
int32 gold = 100;
dbBind.BindParam(0, gold);
WCHAR name[100] = L"이름이름이름";
dbBind.BindParam(1, name);
TIMESTAMP_STRUCT ts = {2023, 8, 29};
dbBind.BindParam(2, ts);
ASSERT_CRASH(dbBind.Execute());
GDBConnectionPool->Push(dbConn);
}
확실히 직접 작성하는 부분이 많이 줄었다.
읽어 들이는 부분은 아래와 같다.
// Read
{
DBConnection* dbConn = GDBConnectionPool->Pop();
DBBind<1, 4> dbBind(*dbConn, L"SELECT id, gold, name, createDate FROM [dbo].[Gold] WHERE gold = (?)");
int32 gold = 100;
dbBind.BindParam(0, gold);
int32 outId = 0;
int32 outGold = 0;
WCHAR outName[100];
TIMESTAMP_STRUCT outDate = {};
dbBind.BindCol(0, OUT outId);
dbBind.BindCol(1, OUT outGold);
dbBind.BindCol(2, OUT outName);
dbBind.BindCol(3, OUT outDate);
ASSERT_CRASH(dbBind.Execute());
wcout.imbue(locale("kor"));
while (dbConn->Fetch())
{
wcout << "Id: " << outId << " Gold : " << outGold << " Name: " << outName << endl;
wcout << "Date : " << outDate.year << "/" << outDate.month << "/" << outDate.day << endl;
}
GDBConnectionPool->Push(dbConn);
}
인덱스와 변수만 넣어주면 끝나니 정말 편해졌다.
결과가 잘 나오나 보자.
숫자는 물론 UTF16 문자열과 날짜도 제대로 출력된다.
'Study > C++ & C#' 카테고리의 다른 글
[C++ / DB] ORM (0) | 2023.09.01 |
---|---|
[C++ / DB] XML Parser (0) | 2023.08.31 |
[C++ / DB] DBConnection (0) | 2023.08.29 |
[C++] JobTimer (0) | 2023.08.27 |
[C++] JobQueue (3 / 3) (0) | 2023.08.27 |