[C++ / DB] DBBind

2023. 8. 29. 22:18·Study/C++ & C#

저번 시간의 불편함을 어느 정도 해소해 보자.

 

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 }; };

 

아래와 같은 순서로 체크가 진행된다.

  1. FullBits<3>가 있다고 가정
  2. FullBits<3> = (1 << 2) | FullBits<2> 라는 형태로 볼 수 있다.
  3. Fullbits<2> = (1 << 1) | FullBits<1>의 형태가 되는 것이다.
  4. 궁극적으로 FullBIts<3> = (1 << 2) | (1 << 1) | (1 << 0)이 된다.
  5. 이 _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
'Study/C++ & C#' 카테고리의 다른 글
  • [C++ / DB] ORM
  • [C++ / DB] XML Parser
  • [C++ / DB] DBConnection
  • [C++] JobTimer
BVM
BVM
  • BVM
    E:\
    BVM
  • 전체
    오늘
    어제
    • 분류 전체보기 (173)
      • Thoughts (14)
      • Study (75)
        • Japanese (3)
        • C++ & C# (50)
        • Javascript (3)
        • Python (14)
        • Others (5)
      • Play (1)
        • Battlefield (1)
      • Others (10)
      • Camp (73)
        • T.I.L. (57)
        • Temp (1)
        • Standard (10)
        • Challenge (3)
        • Project (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 본 블로그 개설의 목적
  • 인기 글

  • 태그

    7계층
    JS
    Network
    Dalamud
    IOCP
    Server
    FF14
    Asio
    docker
    discord py
    암호화
    Python
    discord
    포인터
    c#
    스타필드
    클라우드
    로깅
    C++
    cloudtype
    db
    네트워크 프로그래밍
    베데스다
    boost
    프로그래머스
    서버
    Selenium
    OSI
    네트워크
    bot
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
BVM
[C++ / DB] DBBind
상단으로

티스토리툴바