Poco lib를 이용한 SQLite 사용하기

SQLite를 통해서 C++ 프로그램에서 SQLite를 접근하는 방법에 대한 문서이다

Poco Library는?

SQLite은 C기반이기 때문에 객체기반이지 않아서, 객체로 랩핑한 라이브러리가 있을거라는 예상하에 구글링을 했다.

다양한 라이브러리가 나왔는데, 다들 장단점이 있었다. 그 중에서 Poco라이브러리는 SQLite뿐만 아니라, MySQL, Oracle도

같은 형식으로 랩핑을 하고 있어서, 타 DBMS로 이전시 용이하고, 통일된 인터페이스를 제공하고 있었다.

또한 공짜 ( 사실 라이센스가 조금 복잡한데, 그냥 패스 -_- )이고, DB 랩핑 말고도 다른 부가적인 멋진 기능들을 같이 묶어놓았다.

단점이라면, 플랫폼 수준의 기능을 제공하는 미들웨어 같은 느낌이라, 덩치가 크다.

아래 문서는 Podo Doc을 한글로 바꾸고 정리한 내용이므로 자세한 내용은 http://pocoproject.org/docs/ 를 참고하면된다

예제 살펴보기

Poco Data는 Poco 데이터베이스 추상 레이어로 서로 다른 다양한 데이터 베이스의 데이터를 저장/읽기를 쉽게 해준다.아래는 기본 예제이다.

#include "Poco/Data/Common.h"
#include "Poco/Data/SQLite/Connector.h"
#include <iostream>

using namespace Poco::Data;

void init()
{
    SQLite::Connector::registerConnector();
}

void shutdown()
{
    SQLite::Connector::unregisterConnector();
}

int main(int argc, char* argv[])
{
    init();
    Session ses("SQLite", "sample.db");
    int count = 0;
    ses << "SELECT COUNT(*) FROM PERSON", into(count), now;
    std::cout << "People in DB " << count;
    shutdown();
}

위 예제는 보여주기 위한 것이다. Poco/Data/Common.h 파일은 인클루드 해주자. SQLite::Connector는 SQLite Connector를 등록하는데 사용된다. 이걸 해주어야 나중에 SQLite Session을 SessionFactory로 부터 생성할 수 있게 해준다. 생성자는 2개의 파라미터가 필요하다

Sesssion ses(“SQLite”, “sample.db”);

위 코드는 아래 코드와 동일하다

Session ses(SessionFactory::instance()::create(“SQLite”, “sample.db”));

<< 오퍼레이터는 SQL statements 를 Session으로 보내는데 사용된다. into(count) 는 쿼리의 결과가 어디에 저장되어야 하는지를 간단하게 session에게 알려준다. SQL statement의 마지막에 now라는 것을 주목하라. 이것을 집어 넣지 않으면 SQL 구문은 실행되지 않는다.

 

세션 생성하기

세션은 항상 SessionFactory create 메소드를 통해서 생성된다. 또는 암시적으로 Session 생성자의 2개의 파라미터를 통해서 생성된다.

데이터 추가하기 / 읽어오기

우선 아래와 같은 DB Table이 하나 있다고 가정하자.

ForeName (Name VARCHAR(30))

만약 1개의 데이터를 집어 넣으려고 한다면 간단하게 아래 처럼 하면 된다.

std::string aName(“Peter”);
ses << “INSERT INTO FORENAME VALUES(” << aName << “)”, now;

글쎄… 우리는 위에 처럼 할수 있지만, 우리가 원하는 건 아닐것이다. 더 좋은 솔루션은 placeholder를 사용하는 것이다. placeholder는 이름 앞에 : 를 붙여서 표현한다. 위의 코드를 placeholder를 활용한 코드로 다시 작성해보자.

std::string aName(“Peter”);
ses << “INSERT INTO FORENAME VALUES(:name)”, use(aName), now;

이 예제에서 :name이 Peter 값와 일치하도록 표현되고 있다. 위 예제는 placeholder를 사용하고 있지만, 진정한 placeholder의 장점 ( 성능 개선 )을 보여주고 있지 않는다. Statements 섹션을 읽어보면 좀 더 알 수 있다.

데이터베이스로 부터 데이터를 읽어오는 작업도 비슷하다. into 문법은 데이터베이스로 부터 리턴된 값을 c++ 객체에 넣도록 해준다. into는 기본값을 가질 수 있는데, 데이터베이스로 부터 null 값이 리턴이 되면 기본값을 가지도록 할 수 있다.

std::string aName;
ses << “SELECT NAME FROM FORENAME”, into(aName), now; // the default is the empty string
ses << “SELECT NAME FROM FORENAME”, into(aName, “default”), now;
into 와 use 문법을 같이 사용하는 것도 가능하다std::string aName;
std::string match(“Peter”)
ses << “SELECT NAME FROM FORENAME WHERE NAME=:name”, into(aName), use(match), now;
poco_assert (aName == match);

일반적으로, 테이블 구조는 위에 처럼 간단하지 않다. 보통 여러개의 컬럼을 가지고 있다. 따라서 여러개의 into/use를 사용해야 한다. 아래 예제는 Person 테이블이 age, firstname, lastname을 가지고 있다고 가정한다.

std::string firstName(“Peter”);
std::string lastName(“Junior”);
int age = 0;
ses << “INSERT INTO PERSON VALUES (:fn, :ln, :age)”, use(firstName), use(lastName), use(age), now;
ses << “SELECT (firstname, lastname, age) FROM Person”, into(firstName), into(lastName), into(age), now;

여기에서 가장 중요한 점은 into 와 use 수식이 사용된 순서이다. 처음 나온 placeholder는 처음에 위치한 use와 매치된다. 두번째 placeholder는 두번째 use와 매치된다. 이것은 into 수식에서도 동일하게 적용된다. 예제에서 우리는 firstname을 첫번째 컬럼으로 지정했고 따라서 이것은 into(fitstName)으로 들어가게 된다.

NULL 엔트리 핸들링하기

데이터베이스에서 일반적인 경우로 optinal data field가 NULL을 담고 있는 경우가 있다. NULL을 수용하기 위해서는 into 문법은 항상 defalut 값을 가지고 있어야 한다. 예를 들어서, age 가 optional field라고 가정하고, 기본값으로 -1을 가지도록 한다면, into(age, -1) 이라고 적으면 된다.

std::string firstName(“Peter”;
std::string lastName(“Junior”);
int age = 0;
ses << “INSERT INTO PERSON VALUES (:fn, :ln, :age)”, use(firstName), use(lastName), use(age), now;
ses << “SELECT (firstname, lastname, age) FROM Person”, into(firstName), into(lastName), into(age, -1), now;

statements로 동작하기

우리는 위에서 자주 statment를 언급했었다. 아직 우리는 session 객체로 동작시키는 법만 알고 있었다. 사실 우리는 이미 statments를 이용해서 동작시키고 있었다. 여기서 session의 << 오퍼레이션의 정의를 살펴보자.

template
Statement Session::operator << (const T& t)

위 코드에서 보는 것처럼 << 오퍼레이터는 내부적으로 Statement를 생성하고, 리턴하고 있음을 알 수 있다. 기존에 살펴본 예제에서는 리턴된 statement는 다른 변수에 다시 저장되지 않고, now 문법 때문에 곧바고 수행되고 있었다. 그리고나서 statement는 destroy 되었다. 이제 예전 방식을 statement에 assign하는 것으로 변경해보자.

std::string aName(“Peter”);
Statement stmt = ( ses << “INSERT INTO FORENAME VALUES(:name)”, use(aName) );

소괄호를 둘러싼것을 주목하라. 이렇게 하지 않으면 컴파일러가 에러를 발생시킨다. 만약 위의 문법이 맘에 들지 않는다면, 아래의 코드도 동일한 효과를 낸다.

Statement stmt(ses);
stmt << “INSERT INTO FORENAME VALUES(:name)”, use(aName);
위에 처럼 하면 변수에 값이 저장이 될까? 그렇지 않다. 이제부터는 실행과 제어가 분리되어 있다.
std::string aName(“Peter”);
Statement stmt = ( ses << “INSERT INTO FORENAME VALUES(:name)”, use(aName) );
stmt.execute();
poco_assert (stmt.done());

execute()함수를 호출함으로써 SQL 문법은 실행이 된다. stmt.done() 함수를 통해서 간단하게 잘 수행되었는지를 확인할 수 있다.

준비된 Statements

statement 준비는 now 문법을 제외시키는 것 외에는 동일하다.

Statement stmt = ( ses << “INSERT INTO FORENAME VALUES(:name)”, use(aName) );

준비된 statements를 사용하는 이점은 성능이다. 아래 예제를 보자.

std::string aName();
Statement stmt = ( ses << “INSERT INTO FORENAME VALUES(:name)”, use(aName) );
for (int i = 0; i < 100; ++i)
{
aName.append(“x”);
stmt.execute();
}

SQL 쿼리를 100회를 파싱하고 생성하는 대신에, 오직 한번만 생성하고 placeholder 와 use 문법의 조합으로 인해 100개의 다른 값을 데이터베이스에 넣을수가 있었다. 하지만 여전히 위 예제는 values of collection을 데이터베이스에 집어 넣는 최고의 방법은 아니다.

하지 말아야 할 것

use문법은 reference 변수를 입력으로 받아서, 나중에 execute()가 수행될 때 처리된다. 따라서 오직 변수만을 사용할 수 있으며, 상수를 사용할 수 없다. 아래 코드는 실패할 것이다.

Statement stmt = (ses << “INSERT INTO PERSON VALUES (:fn, :ln, :age)”, use(“Peter”), use(“Junior”), use(4)); //ERR!
stmt.execute();

콜렉션 지원

만약 한번에 많은 변수를 핸들링할 필요가 있다면, collection 클래스를 사용하는 것이 방법이다. 기본적으로 아래와 같은 collection type을 지원한다.

vector: 필요한 사항 없음
set: < 오퍼레이터는 반드시 데이터 형식에 의해 지원되어야 한다. 중복된 key/value는 무시된다.
multiset: < 오퍼레이터는 반드시 데이터 형식에 의해 지원되어야 한다.
map: () 오퍼레이션은 반드시 데이터 형식에 의해 지원되어야 하고, 객체의 키값을 리턴해야 한다. 중복되는 key/value값은 무시된다.
multimap : () 오퍼레이션은 반드시 데이터 형식에 의해 지원되어야 하고, 객체의 키값을 리턴해야 한다.

벡터를 이용한 대량으로 삽입하는 예제이다.

std::string aName(“”);
std::vector data;
for (int i = 0; i < 100; ++i)
{
aName.append(“x”);
data.push_back(aName);
}
ses << “INSERT INTO FORENAME VALUES(:name)”, use(data), now;

동일한 예제가 set, multiset에도 적용이 된다. 하지만 map과 multimap은 ()오퍼에이터가 없어서 적용되지 않는다.

use 문법이 비어있지 않는 collection만을 처리한다는 점을 기억하라.

아래 예제를 살펴보자.

std::string aName;
ses << “SELECT NAME FROM FORENAME”, into(aName), now;

이전에, 1개의 엔트리만을 담고 있기 때문에 위의 코드가 동작하는 것을 알 수 있었다. 하지만 이제는 데이터베이스 테이블이 최소한 100개의 스트링을 담고 있다. 위 코드는 단지 1개의 결과값만을 저장할 수 있는 저장공간을 제공한다. 따라서 위 코드는 exception을 발생시킬 것이다. 아래 예제는 해결 방법중 하나이다.

std::vectornames;
ses << “SELECT NAME FROM FORENAME”, into(names), now;

limit 구문

collection을 이용한 동작은 대량의 데이터를 처리하게 될것이고, 동시에 대량의 데이터 처리는 어플리케이션을 오랜 시간 동안 block상태로 만들어 버릴것이다. limit 키워드를 사용하면 문제를 해결 가능 하다.

데이터베이스에서 수천 row를 가져와서 GUI에 그린다고 가정하자. 사용자는 언제나 데이터를 가져오는 것을 정지할 수 있다. 이럴경우 우리는 아래처럼 프로세스를 분리해야 한다.

std::vectornames;
ses << “SELECT NAME FROM FORENAME”, into(names), limit(50), now;

위 예제는 50개의 row만을 데이터베이스에서 가져와서 저장하는 예제이다.
만약 names collection이 비어있지 않았었는데, 그 안에 정확히 50개의 데이터만을 담도록 하고 싶다면 limit 파라미터의 2번째를 true로 해주어야 한다. (기본적으로는 false)

std::vector names;
ses << “SELECT NAME FROM FORENAME”, into(names), limit(50, true), now;

statement.done()이 true를 리턴할 때 까지 collection을 반복한다. 아래 예제는 테이블에 101개의 데이터가 들어있다고 가정한다.

std::vectornames;
Statement stmt = (ses << “SELECT NAME FROM FORENAME”, into(names), limit(50));
stmt.execute(); //names.size() == 50
poco_assert (!stmt.done());
stmt.execute(); //names.size() == 100
poco_assert (!stmt.done());
stmt.execute(); //names.size() == 101
poco_assert (stmt.done());

위에서 우리는 데이터 없음을 리턴하는 것도 정상인것을 가정했다. 아래처럼 실행하면 테이블이 비어있는 경우에도 잘 동작을 한다.

std::string aName;
ses << “SELECT NAME FROM FORENAME”, into(aName), now;

최소한 1개 이상의 데이터가 있어야 됨을 보장하려면 lowerLimit을 걸면 된다.

std::string aName;
ses << “SELECT NAME FROM FORENAME”, into(aName), lowerLimit(1), now;

만약 테이블이 비어있다면, exception이 발생하게 된다. 한개씩 결과를 계속 뽑아내고 싶다면, 예를 들어 collection을 사용하고 싶지 않다면, 아래 처럼 작성하면 된다.

std::string aName;
Statement stmt = (ses << “SELECT NAME FROM FORENAME”, into(aName), lowerLimit(1), upperLimit(1));
while (!stmt.done())
stmt.execute();

또다른 방법은 range를 이용하는 것이다.

std::string aName;
Statement stmt = (ses << “SELECT NAME FROM FORENAME”, into(aName), range(1,1));
while (!stmt.done())
stmt.execute();

복잡한 데이터 형식 매핑

위에서 살펴본 모든 예제는 기본 데이터 형식을 가지고 작업하는 것이었다. 하지만 현실에서는 그렇지 못하다. 여기서는 당신이 Person class를 가지고 있다고 가정하자.

class Person
{
public:
    // default constructor+destr.
    // getter and setter methods for all members
    [...]

    bool operator <(const Person& p) const
        /// we need this for set and multiset support
    {
        return _socialSecNr < p._socialSecNr;
    }

    Poco::UInt64 operator()() const
        /// we need this operator to return the key for the map and multimap
    {
        return _socialSecNr;
    }

private:
    std::string _firstName;
    std::string _lastName;
    Poco::UInt64 _socialSecNr;
}

필요한 것은 TypeHandler 템플릿의 형식에 맞춰 template specialization 해주는 것이다. 주의할 점은 템플릿 specialization은 반드시 original template와 같은 namespace에 존재 해야 한다는 점이다(Poco::Data). template specialization은 반드시 아래 메소드들을 구현해주어야 한다.

namespace Poco {
namespace Data {

template <>
class TypeHandler<class Person>
{
public:
    static std::size_t size()
    {
        return 3; // we handle three columns of the Table!
    }

   static void bind(std::size_t pos, const Person& obj, AbstractBinder* pBinder)
    {
        poco_assert_dbg (pBinder != 0);
        // the table is defined as Person (FirstName VARCHAR(30), lastName VARCHAR, SocialSecNr INTEGER(3))
        // Note that we advance pos by the number of columns the datatype uses! For string/int this is one.
        TypeHandler<std::string>::bind(pos++, obj.getFirstName(), pBinder);
        TypeHandler<std::string>::bind(pos++, obj.getLastName(), pBinder);
        TypeHandler<Poco::UInt64>::bind(pos++, obj.getSocialSecNr(), pBinder);
    }

    static void prepare(std::size_t pos, const Person& obj, AbstractPreparation* pPrepare)
    {
        poco_assert_dbg (pBinder != 0);
        // the table is defined as Person (FirstName VARCHAR(30), lastName VARCHAR, SocialSecNr INTEGER(3))
        // Note that we advance pos by the number of columns the datatype uses! For string/int this is one.
        TypeHandler<std::string>::prepare(pos++, obj.getFirstName(), pPrepare);
        TypeHandler<std::string>::prepare(pos++, obj.getLastName(), pPrepare);
        TypeHandler<Poco::UInt64>::prepare(pos++, obj.getSocialSecNr(), pPrepare);
    }

    static void extract(std::size_t pos, Person& obj, const Person& defVal, AbstractExtractor* pExt)
        /// obj will contain the result, defVal contains values we should use when one column is NULL
    {
        poco_assert_dbg (pExt != 0);
        std::string firstName;
        std::string lastName;
        Poco::UInt64 socialSecNr = 0;
        TypeHandler<std::string>::extract(pos++, firstName, defVal.getFirstName(), pExt);
        TypeHandler<std::string>::extract(pos++, lastName, defVal.getLastName(), pExt);
        TypeHandler<Poco::UInt64>::extract(pos++, socialSecNr, defVal.getSocialSecNr(), pExt);
        obj.setFirstName(firstName);
        obj.setLastName(lastName);
        obj.setSocialSecNr(socialSecNr);
    }
};
} } // namespace Poco::Data

이것이 당신이 해야할 모든 작업이다. 이제 아래처럼 간단하게 Person을 사용하면 된다.

std::mappeople;
ses << “SELECT * FROM Person”, into(people), now;

RecordSet

Poco::Data::RecordSet class는 데이터베이스 테이블과 같이 동작하는 일반적인 방법을 제공한다. RecordSet을 이용하면 아래 사항이 가능하다.

모든 column과 row의 interation
column에 대한 메타 데이터를 담는다.(이름, 형식, 길이 등등)

RecordSet을 쓰려면, 첫번째로 Statement를 생성하고, 실행한 후 Statement로 부터 RecordSet를 생성한다. 예제이다.

Statement select(session);
select << “SELECT * FROM Person”;
select.execute();
RecordSet rs(select);

아래 예제는 어떻게 모든 row와 column을 처리할 수 있는지를 보여준다.

bool more = rs.moveFirst();
while (more)
{
for (std::size_t col = 0; col < cols; ++col)
{
std::cout << rs[col].convert() << ” “;
}
std::cout << std::endl;
more = rs.moveNext();
}

이 예제는 limit과 같이 조합해서 사용하는 예제이다.

Statement select(session);
select << “SELECT * FROM Person”, range(0, 10);
RecordSet rs(select);
while (!select.done())
{
select.execute();
bool more = rs.moveFirst();
while (more)
{
for (std::size_t col = 0; col < cols; ++col)
{
std::cout << rs[col].convert() << ” “;
}
std::cout << std::endl;
more = rs.moveNext();
}
}

Tuples

Poco::Tuple 과 Poco::Tuple의 vector는 column 타입들을 이미 알고 있을 경우 간편한 방법을 제공한다.아래 코드를 살펴보자.

typedef Poco::Tuple Person;
typedef std::vector People;

People people;
people.push_back(Person(“Bart Simpson”, “Springfield”, 12));
people.push_back(Person(“Lisa Simpson”, “Springfield”, 10));

Statement insert(session);
insert << “INSERT INTO Person VALUES(:name, :address, :age)”,
use(people), now;

아래 코드도 가능하다

Statement select(session);
select << “SELECT Name, Address, Age FROM Person”,
into(people),
now;

for (People::const_iterator it = people.begin(); it != people.end(); ++it)
{
std::cout << “Name: ” << it->get() <<
“, Address: ” << it->get() <<
“, Age: ” << it->get() < 11.}

Session Pooling

데이터베이스와 연력을 생성하는 작업은 시간을 잡아먹는 작업이다. 따라서 session객체를 다 쓰면 반환받았다가 나중에 다시 써먹는 것은 좋은 방법이다. Poco::Data::SessionPool 은 session 모음을 관리해준다.

SessionPool pool(“ODBC”, “…”);
// …
Session sess(pool.get());

session이 destroy될 때 자동으로 Pool로 반환을 한다.

Advertisements

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

w

%s에 연결하는 중