게임에만 버전이 있는 것이 아니라 DB에도 버전이 있다.
따라서 게임 버전에 DB 버전을 매칭시킬 필요가 있다.
예를 들어서 게임에서 A와 B라는 테이블이 있었는데, 업데이트로 인해 C, D 테이블을 새로 활용하게 됐다.
하지만 DB는 여전히 A와 B, 2개의 테이블만 갖고 있다고 한다면 문제가 생길 수밖에 없을 것이다.
이런 과정을 자동화 할 수 있는 도구를 「Object Relational Mapping (ORM)」이라고 한다.
자체적으로 비교적 단순한 ORM을 만들어 볼 것이다.
엄밀히 말하면 도구라기 보단 방법에 가깝지만 ORM Tool이라고 생각하자.
1. 밑작업
XML과 파일 입출력을 사용할 것이다.
따라서 XML 파서 및 파일 입출력용 함수를 준비할 필요가 있다.
XML 파싱을 위해 RapidXml을 사용할 것이며,
파일 입출력을 위해 C++17 표준인 std::filesystem
을 사용할 것이다.
라이브러리를 다운받아 프로젝트에 추가하고 아래와 같은 클래스를 작성한다.
// XmlParser.h
#pragma once
#include "Types.h"
#include "Container.h"
#include "rapidxml.hpp"
using namespace rapidxml;
using XmlNodeType = xml_node<WCHAR>;
using XmlDocumentType = xml_document<WCHAR>;
using XmlAttributeType = xml_attribute<WCHAR>;
class XmlNode
{
public:
XmlNode(XmlNodeType* node = nullptr) : _node(node) { }
bool IsValid() { return _node != nullptr; }
bool GetBoolAttr(const WCHAR* key, bool defaultValue = false);
int8 GetInt8Attr(const WCHAR* key, int8 defaultValue = 0);
int16 GetInt16Attr(const WCHAR* key, int16 defaultValue = 0);
int32 GetInt32Attr(const WCHAR* key, int32 defaultValue = 0);
int64 GetInt64Attr(const WCHAR* key, int64 defaultValue = 0);
float GetFloatAttr(const WCHAR* key, float defaultValue = 0.0f);
double GetDoubleAttr(const WCHAR* key, double defaultValue = 0.0);
const WCHAR* GetStringAttr(const WCHAR* key, const WCHAR* defaultValue = L"");
bool GetBoolValue(bool defaultValue = false);
int8 GetInt8Value(int8 defaultValue = 0);
// ...
XmlNode FindChild(const WCHAR* key);
Vector<XmlNode> FindChildren(const WCHAR* key);
private:
XmlNodeType* _node = nullptr;
};
class XmlParser
{
public:
bool ParseFromFile(const WCHAR* path, OUT XmlNode& root);
private:
shared_ptr<XmlDocumentType> _document = nullptr;
String _data;
};
// XmlParser.cpp
#include "pch.h"
#include "XmlParser.h"
#include "FileUtils.h"
#include "CoreGlobal.h"
_locale_t kr = _create_locale(LC_NUMERIC, "kor");
bool XmlNode::GetBoolAttr(const WCHAR* key, bool defaultValue)
{
XmlAttributeType* attr = _node->first_attribute(key);
if (attr)
return ::_wcsicmp(attr->value(), L"true") == 0;
return defaultValue;
}
int8 XmlNode::GetInt8Attr(const WCHAR* key, int8 defaultValue)
{
XmlAttributeType* attr = _node->first_attribute(key);
if (attr)
return static_cast<int8>(::_wtoi(attr->value()));
return defaultValue;
}
// ...
노드 단위로 각 속성(Attribute) 및 값(Value)을 찾아서 가져올 수 있다.
파일 입출력 클래스는 아래와 같다.
// FileUtils.h
#pragma once
#include <vector>
#include "Types.h"
class FileUtils
{
public:
static Vector<BYTE> ReadFile(const WCHAR* path);
static String Convert(string str);
};
// FileUtils.cpp
#include "pch.h"
#include "FileUtils.h"
#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
Vector<BYTE> FileUtils::ReadFile(const WCHAR* path)
{
Vector<BYTE> ret;
fs::path filePath{ path };
const uint32 fileSize = static_cast<uint32>(fs::file_size(filePath));
ret.resize(fileSize);
basic_ifstream<BYTE> inputStream{ filePath };
inputStream.read(&ret[0], fileSize);
return ret;
}
String FileUtils::Convert(string str)
{
const int32 srcLen = static_cast<int32>(str.size());
String ret;
if (srcLen == 0)
return ret;
const int32 retLen = ::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<char*>(&str[0]), srcLen, NULL, 0);
ret.resize(retLen);
::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<char*>(&str[0]), srcLen, &ret[0], retLen);
return ret;
}
C++17
부터 추가된 filesystem
덕에 더 이상 윈도우나 리눅스 의존적인 기능들을 사용할 필요가 없어졌다.
그리고 이 프로젝트에서 모든 문자열은 UTF-16
으로 간주할 것이기 때문에,
UTF-8
로 작성되는 파일에 대해서 변환이 필요하기 때문에 Convert()
함수가 추가되었다.
2. XML 작성
서버 코드 내에 하드코딩 했던 부분들을 XML 파일로 옮겨보자.
<?xml version="1.0" encoding="utf-8"?>
<GameDB>
<Table name="Gold" desc="골드 테이블">
<Column name="id" type="int" notnull="true" />
<Column name="gold" type="int" notnull="false" />
<Column name="name" type="nvarchar(50)" notnull="false" />
<Column name="createDate" type="datetime" notnull="false" />
<Index type="clustered">
<PrimaryKey/>
<Column name="id" />
</Index>
</Table>
<Procedure name="spInsertGold">
<Param name="@gold" type="int"/>
<Param name="@name" type="nvarchar(50)"/>
<Param name="@createDate" type="datetime"/>
<Body>
<!-- CDATA로 묶어서 따로 파싱하지 않고 하나의 데이터로 간주하게 한다 -->
<![CDATA[
INSERT INTO [dbo].[Gold]([gold], [name], [createDate]) VALUES(@gold, @name, @createDate);
]]>
</Body>
</Procedure>
<Procedure name="spGetGold">
<Param name="@gold" type="int"/>
<Body>
<![CDATA[
SELECT id, gold, name, createDate FROM [dbo].[Gold] WHERE gold = (@gold)
]]>
</Body>
</Procedure>
</GameDB>
- GameDB라는 큰 틀이 존재.
Table
및Procedure
로 구분된다.- Procedure
- 함수처럼 호출할 수 있음
- 쿼리문을 편리하게 사용하기 위함
-CDATA
로 싸여진 부분은 별도의 파싱 과정 없이 그 안의 내용을 하나의 데이터로 간주하라는 의미 - 이름에 「@」가 붙은 것은 변수처럼 다른 곳에서 사용할 수 있게 하기 위함이다.
3. 결과 확인
XML 파싱이 정상적으로 이루어지나 확인한다.
파싱이 정상적으로 이루어짐을 알 수 있다.
'Study > C++ & C#' 카테고리의 다른 글
[C++ / Python / DB] ProcedureGenerator (0) | 2023.09.01 |
---|---|
[C++ / DB] ORM (0) | 2023.09.01 |
[C++ / DB] DBBind (0) | 2023.08.29 |
[C++ / DB] DBConnection (0) | 2023.08.29 |
[C++] JobTimer (0) | 2023.08.27 |