Повноцінний веб-сайт на C++ і трохи диванної аналітики

Але навіщо?Тут повинна бути картинка про тролейбус

Нечемно відповідати питанням на питання, але: а чому б і ні? Просто тому, що можна.
Гаразд, я пожартував. Щоб пояснити причину, хотілося б коротко описати історію мого знайомства з веб-розробкою. Але, щоб не порушувати послідовність розповіді, я вирішив помістити її в кінці. Загалом, з причинами ми ще розберемося.

Думаю, багатьом знайома така різновид веб-форумів, як имиджборды. Так-так, ви правильно зрозуміли — саме на прикладі имиджборды я розповім про досвід створення сайту на C++. Що ж змусило мене зайнятися настільки сумнівної користі проектом? Ліва пятка. В цьому випадку дійсно ніяких особливих причин не було. Просто прокинувся одного ранку і зрозумів — хочу. Але це все лірика.

На Хабре вистачає статей про веб-сайтах на C++: наприклад, з використанням FastCGI або CppCMS. Але все це — HelloWorld'и та туторіали. Я ж вам розповім про повноцінне (нехай і не ідеальний з точки зору архітектури і чистоти коду) проекті, постараюся висвітлити різні тонкощі.

Підготовка
Почнемо з інструментів, які нам потрібні. Варто відразу зауважити, що я використовую для своїх проектів Qt і навіть написав невелику бібліотеку, яка розширює можливості цього чудового фреймворка. Qt і ця бібліотека (називається, до речі, BeQt — Beyond Qt) використовувалися мною і в цей раз, але на них я зупинятися не буду, так як стаття про інше.

Веб-фреймворк

Отже, перш за все, звичайно ж, потрібно вирішити, який веб-фреймворк використовувати. Вибір невеликий, але він все ж є:
Про перших двох я можу сказати тільки те, що мені вони не сподобалися. Це не означає, що дані фреймворки погані — я з ними не працював і не знаю.
Wt я пробував пару років тому. Я тоді спокусився схожою з Qt архітектурою та цікавою особливістю: можна було, нічого не знаючи про веб-додатках і HTML, створювати ієрархію віджетів, яка потім без жодних шаблонів рендерилась в цей HTML. З вебом я тоді був знайомий на рівні «написати в блокноті HTML-сторінку з title'ом і двома параграфами», тобто, можна сказати, був не знайомий зовсім. Однак швидко з'ясувалося, що для написання чогось складніше горезвісного HelloWorld'а таки потрібні шаблони, та ще й якісь сторонні скрипти (.js), які треба качати з «лівих» сайтів. Тому знайомство з Wt дуже скоро закінчилося.
Але через деякий час, коли я вже познайомився з вебом, JavaScript, CSS, AJAX-запитів, — тоді я вирішив ще раз зайнятися темою «веб-сайт на C++». Знову погугливши, я натрапив на CppCMS. Цей фреймворк мені сподобався відразу: документація (нехай і убога місцями) — є, туторіали — є (причому дуже хороші, дуже практичні приклади, а не притягнуті за вуха), крос-платформеність — є, шаблони — є (тепер я вже розумів, що без них нікуди). Що ще потрібно для щастя? Скачав, скомпілював, підключив до проекту, перевірив, порадів.

ORM-фреймворк

Далі постає питання про зберігання даних. В Qt є модуль QtSql, але він передбачає досить низький рівень абстракції — вручну пишемо запити, вручну обробляємо результати. Мені ж хотілося ORM-рішення. Нагуглил я, не рахуючи різних недоробок і занедбаних проектів, наступне (прошу вибачення, якщо пропустив якийсь стоїть фреймворк — ніхто не всезнаючий):
LiteSQL мені не сподобався тим, що на сайті я не знайшов документацію, а нагуглил тільки щось з XML-конфігурацією (вже вибачте, але XML очі б мої не бачили). QxOrm я відкинув по випадковості і дурості: зайшов на сайт, побачив скріншот графічного дизайнера зв'язків і закрив сторінку. Вже деякий час опісля, прочитавши статтю на Хабре, я ще раз вирішив придивитися до цього фреймворку, і він мені сподобався, але зробленого не повернеш. Поспішиш, як кажуть…
ODB мені більш-менш сподобався, хоч і не знайшлося документації по класах, зате були досить докладні приклади. Крім того, ODB підтримує типи Qt, такі як QString, QDateTime та інші. Це дуже зручно — не потрібно перетворювати туди-сюди. На цьому фреймворку і зупинив свій вибір.
Хвилинка гуморуТут повинна бути картинка про ODB

Підсвічування синтаксису

Що мені не подобається в більшості великих имиджборд — немає можливості толком вставляти код. Або не зовсім (з-за особливостей розмітки деякі символи з'їдаються, роблячи шрифт укладеного між ними тексту курсивним або підкресленим), або можна, але без підсвічування синтаксису. Тому я ще до початку проекту вирішив, що така можливість у мене обов'язково повинна бути присутня. Вважаю, що така фіча згодиться на багатьох IT-сайтах.
Перше, що я нагуглил — hilite.me — онлайн-сервіс, що дозволяє перетворювати переданий у запиті текст в HTML, що містить той же текст, але відформатований з підсвічуванням синтаксису. Для роботи з цим сайтом я спершу хотів скористатися QtNetwork, а конкретно — QNetworkAccessManager. Однак, оскільки CppCMS обробляє запити в окремих потоках, поза QThread, використовувати цей механізм не вийшло (сигнали і слоти поза QEventLoop не працюють, а блокуючого API у QNetworkReply немає). Довелося чіпляти нові бібліотеки: libcurl (тут, наскільки мені відомо, без варіантів) і cURLpp — обгортка над libcurl для C++ (був ще варіант curlplusplus, але я вибрав cURLpp).
Тут я вимушено забіжу вперед. Спочатку я зрадів, що підсвічування працює як треба, але потім я став помічати, що дуже вже довго все це працює: поки з'єднається з сервісом, поки той обробить запит, поки надішле відповідь… Ну його. І знову я напружив гугл. Ось що знайшлося в цей раз: Source-highlight. Крім консольної утиліти цей проект також надає бібліотеку з тією ж функціональністю (назва говорить сама за себе). Те що треба! (Зауважу, однак, що, хоча бібліотека і виконує свою роботу без нарікань, але набір файлів-визначень для різних мов довелося качати окремо, і в пам'ять файли ніяк не завантажити — бібліотека може працювати з ними, тільки якщо вони лежакт десь у цій файловій системі, а не запаковані, скажімо, у виконуваний файл разом з іншими ресурсами.)

Скажи мені, які твої Magic bytes, і я скажу тобі, хто ти

Мова про визначення типу файлу. Имиджборды — вони ж від слова image в сенсі «картинка», а не імідж, а отже, в самій їхній основі лежить прикріплення зображень до повідомлень. І не тільки зображень — останнім часом все більшу популярність набирає формат WebM. Але хто з нас не намагався хоч раз зробити що-то не за правилами? Рано чи пізно комусь прийде в голову прикріпити замість картинки архів або щось ще. Інформації, що передається клієнтом про MIME-тип файлу, теж вірити не можна, тому потрібно перевіряти його тип якимось незалежним інструментом. Таким, як libmagic, наприклад. Але зверніть увагу на дату останнього оновлення. Тому краще використовувати цю реалізацію — вона підтримує більше нових форматів, у тому числі згадуваний WebM.
Звичайно, Magic bytes не дають ніяких гарантій, що інший вміст файлу відповідає формату, але тут вже нічого не поробиш. Хіба що найняти відділ китайців (не в образу цим трудягам будь сказано), які будуть перевіряти вручну.

Ще б підключити все це...

Ось ми з вибором інструментів і розібралися. А як чіпляти до проекту і використовувати? В Qt, на щастя, є не сама погана (хоч і сама по собі вельми дивна) система складання — qmake.
Ось як виглядає фрагмент файла проекту (.pro/.pri), який відповідає за підключення бібліотек:
Прихований текст
isEmpty(BEQT_PREFIX) {
mac|unix {
BEQT_PREFIX=/usr/share/beqt
} else:win32 {
BEQT_PREFIX=$$(systemdrive)/PROGRA~1/BeQt
}
}
include($${BEQT_PREFIX}/share/beqt/depend.pri)

isEmpty(CPPCMS_PREFIX) {
mac|unix {
CPPCMS_PREFIX=/usr
} else:win32 {
error(CppCMS path is not specified)
}
}

INCLUDEPATH *= $${CPPCMS_PREFIX}/include
DEPENDPATH *= $${CPPCMS_PREFIX}/include
LIBS *= -L$${CPPCMS_PREFIX}/lib/ -lcppcms-lbooster

isEmpty(ODB_PREFIX) {
mac|unix {
ODB_PREFIX=/usr
} else:win32 {
error(ODB path is not specified)
}
}

INCLUDEPATH *= $${ODB_PREFIX}/include
DEPENDPATH *= $${ODB_PREFIX}/include
LIBS *= -L$${ODB_PREFIX}/lib/ -lodb-lodb-sqlite

!isEmpty(ODB_QT_PREFIX) {
INCLUDEPATH *= $${ODB_QT_PREFIX}/include
DEPENDPATH *= $${ODB_QT_PREFIX}/include
LIBS *= -L$${ODB_QT_PREFIX}/lib/ -lodb-qt
} else {
LIBS *= -L$${ODB_PREFIX}/lib/ -lodb-qt
}

isEmpty(LIBCURL_PREFIX) {
mac|unix {
LIBCURL_PREFIX=/usr
} else:win32 {
error(libcurl path is not specified)
}
}

INCLUDEPATH *= $${LIBCURL_PREFIX}/include
DEPENDPATH *= $${LIBCURL_PREFIX}/include
LIBS *= -L$${LIBCURL_PREFIX}/lib/ -lcurl

isEmpty(CURLPP_PREFIX) {
mac|unix {
CURLPP_PREFIX=/usr
} else:win32 {
error(cURLpp path is not specified)
}
}

INCLUDEPATH *= $${CURLPP_PREFIX}/include
DEPENDPATH *= $${CURLPP_PREFIX}/include
LIBS *= -L$${CURLPP_PREFIX}/lib/ -lcurlpp

isEmpty(BOOST_PREFIX) {
mac|unix {
BOOST_PREFIX=/usr
} else:win32 {
BOOST_PREFIX=$$(systemdrive)/Boost
}
}

INCLUDEPATH *= $${BOOST_PREFIX}/include
DEPENDPATH *= $${BOOST_PREFIX}/include
LIBS *= -L$${BOOST_PREFIX}/lib/ -lboost_regex

isEmpty(SRCHILITE_PREFIX) {
mac|unix {
SRCHILITE_PREFIX=/usr
} else:win32 {
error(GNU Source-highlight path is not specified)
}
}

INCLUDEPATH *= $${SRCHILITE_PREFIX}/include
DEPENDPATH *= $${SRCHILITE_PREFIX}/include
LIBS *= -L$${SRCHILITE_PREFIX}/lib/ -lsource-highlight

isEmpty(LIBMAGIC_PREFIX) {
mac|unix {
LIBMAGIC_PREFIX=/usr
} else:win32 {
error(libmagic path is not specified)
}
}

INCLUDEPATH *= $${LIBMAGIC_PREFIX}/include
DEPENDPATH *= $${LIBMAGIC_PREFIX}/include
LIBS *= -L$${LIBMAGIC_PREFIX}/lib/ -lmagic

isEmpty(SQLITE_PREFIX) {
mac|unix {
SQLITE_PREFIX=/usr
} else:win32 {
error(SQLite path is not specified)
}
}

INCLUDEPATH *= $${SQLITE_PREFIX}/include
DEPENDPATH *= $${SQLITE_PREFIX}/include
LIBS *= -L$${SQLITE_PREFIX}/lib/ -lsqlite3

mac|unix {
isEmpty(LORD_PREFIX):LORD_PREFIX=/usr
} else:win32 {
isEmpty(LORD_PREFIX):PREFIX=$$(systemdrive)/PROGRA~1/ololord
}


Крім згаданих бібліотек можна помітити Boost і SQLite, які є серед їх залежностей. Зупинятися на цих бібліотеках не стану — я їх не використав. Про SQLite скажу коротко: я не вперше працюю з цією базою, і так як у мене немає необхідності розміщувати БД на окремому сервері, то я вибрав її з-за простоти.
Звичайно, кожен вибирає інструменти під себе, і той набір, що описано тут, не є рекомендацією. Вибирайте те, що більше подобається (якби я мала можливість повернутися в минуле — вибрав би QxOrm замість ODB).

За роботу

Шляхи

(Росіяни «шляхи» і «маршрути» як не звучать у порівнянні з англійським «routes», але що поробиш.) У CppCMS кожен запит обробляє окреме «додаток» — клас-спадкоємець cppcms::application. Кожен шлях задається регулярним виразом, з яким зіставляється функція-обробник, наприклад:
class MyApplication : public cppcms::application
{
public:
explicit MyApplication(cppcms::service &service) :
cppcms::application(service)
{
dispatcher().assign("/file/\\d+", &MyApplication::handleFile, this, 1);
mapper().assign("/file", "/file/{1}");
}
void handleFile(std::string fileNo)
{
//тут обробляємо запит
}
};

cppcms::service — штука, яка відповідає за створення нових екземплярів cppcms::application, до неї ми ще повернемося. А поки розглянемо дві тонкощі: пріоритет шляхів і ситуацію, коли URL закінчується слешем, або ні.
Имиджборда містить список дощок за адресою "/[a-z]+" (спрощено для наочності). Тобто, наприклад, «site.com/b». А якщо набрати «site.com/b/»? Все буде добре? Ні, нічого доброго не ждіть. CppCMS не створює автоматично alias'ів зі слешем на кінці. І правильно робить. Але, тим не менш, іноді такі alias'и потрібні, і про них не варто забувати (і додавати вручну).
Не варто також забувати і про те, що шляхи мають пріоритет відповідно до порядку їх додавання: чим раніше додано шлях, тим вище його пріоритет. Тому, якщо написати так:
dispatcher().assign("/.+", &MyApplication::handleAnything, this, 1);
dispatcher().assign("/page", &MyApplication::handlePage, this);

сторінка «site.com/page» виявиться недоступною, так як її URL підходить під регулярний вираз "/.+", і обробник для нього встановлений з більш високим пріоритетом. Правильно було б призначити боработчики в зворотному порядку. Важливо пам'ятати про цьому моменті.
Тепер до реального прикладу. Робота з шляхами організована у мене? Насамперед уявімо, що потрібно додати якусь свою сторінку зі своїм URL. Правити исходники? Вибачте. Вводимо підтримку плагінів-фабрик, які створюють список структур, у кожній з яких міститься ругулярное вираз, відповідна функція-обробник, кількість аргументів (щоб викликати потрібний метод) і пріоритет. Якщо шляхи збігаються з вже наявними за замовчуванням, то шляху з плагінів їх замінюють. Повний код можна подивитися тут (обережно, локшина): клас-спадкоємець cppcms::application, шляхи, інтерфейс плагіна.
Тут як раз використовується моя надбудова над Qt, що дозволяє автоматично завантажувати плагіни певного типу (перевірка реалізована за допомогою окремого інтерфейсу) з декількох папок: загальносистемних і користувальницьких (наприклад, "/usr/lib/app/plugins" і "/home/user/.app/lib/app/plugins"). Не будемо зупинятися на цьому.

cppcms::service і конфігурація

Як вже говорилося вище, cppcms::service відповідає за створення нових екземплярів cppcms::application, які вже у свою чергу обробляють запити, що надходять на сервер. Щоб cppcms::service міг створювати екземпляри вашого нащадка cppcms::application, його потрібно зареєструвати:
service.applications_pool().mount(cppcms::applications_factory<MyApplication>());

cppcms::service блокує поточний тред і запускає по мірі необхідності нові треди, в яких працюють примірники cppcms::application. Щоб не заважати роботі QCoreApplication (основний клас Qt у консольних додатках), я запускаю cppcms::service в окремому тред:
class OlolordWebAppThread : public QThread
{
private:
const cppcms::json::value Conf;
public:
explicit OlolordWebAppThread(const cppcms::json::value &conf, QObject *parent = 0) :
QThread(parent), F(conf)
{
//
}
protected:
void run()
{
forever {
try {
cppcms::service service(Conf);
service.applications_pool().mount(cppcms::applications_factory<OlolordWebApp>());
service.run();
} catch(std::exception const &e) {
qDebug() << e.what();
}
}
}
};

Зверніть увагу на два моменти: обробку винятків і константу const cppcms::json::value Conf. На відміну від Qt, CppCMS повсюдно використовує винятку. Я не люблю виключення і дотримуюся філософії Qt, коли замість
try {
int x = doSomething();
} catch (const Exception &e) {
//обробляємо помилку
}

використовується
bool ok = false;
int x = doSomething(&ok);
if (!ok) {
//обробляємо помилку
}

Тим не менш, не забувайте про винятки — вони в CppCMS періодично викидаються, і їх потрібно вчасно перехоплювати і обробляти.
Що ж стосується const cppcms::json::value Conf, то це — уявлення JSON-об'єкта, в даному випадку містить конфігурацію сервера. Приклад конфігурації:
{
"service": {
"api": "http",
"порту": 80,
"ip": "0.0.0.0"
}
}

«api» приймає значення «fastcgi», «scgi», або «http», вказує, чи є додаток самостійним (зі своїм HTTP-сервером), або працює під управлінням *CGI.
«port» і «ip» — порт та адреса, додаток слухає. «0.0.0.0» означає будь-яку адресу (інакше кажучи, сервер буде відповідати на запити з усіх адрес).
Подробиці — тут. Хоча рекомендується використовувати *CGI, для мого випадку це було б лише зайвим ускладненням.
Пару слів про те, як отримати cppcms::json::value, скажімо, з файлу. Потрібно скористатися методом load, але він приймає в якості параметра std::istream, тому я написав допоміжну функцію:
Прихований текст
struct membuf : std::streambuf
{
public:
explicit membuf(char *begin, char *end)
{
this->setg(begin, begin, end);
}
};

cppcms::json::value readJsonValue(const QString &fileName, bool *ok)
{
bool b = false;
QByteArray jsonBa = BDirTools::readFile(fileName, -1, &b);
if (!b)
return cppcms::json::value();
char *jsonData = jsonBa.data();
membuf jsonBuff(jsonData, jsonData + jsonBa.size());
std::istream jsonIn(&jsonBuff);
cppcms::json::value json;
if (json.load(jsonIn, true))
return bRet(ok, true, json);
else
return bRet(ok, false, cppcms::json::value());
}

Тут використана одна з функцій моєї бібліотеки, а також маленький трюк std::streambuf. Потрібно це все для того, щоб можна було зчитувати налаштування не тільки з файлу на диску, але і з вбудованих в додаток ресурсів (Qt Resource System).

Тепер, нарешті, запускаємо наш додаток:
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv); //головний клас Qt
cppcms::json::value conf = Tools::readJsonValue("/path/to/conf/file", &ok);
OlolordWebAppThread t(conf);
t.start();
int ret = app.exec(); //запуск QEventLoop
t.terminate(); //примусова зупинка треда, оскільки інших способів в даному випадку немає
t.wait(5 * BeQt::Second); //очікування, так як тред може завершитися не моментально
return ret;
}


Контролери та шаблони

Ось ми і підійшли до найцікавішого — рендерингу відповідей на запити. Не буду зупинятися на малокорисних прикладах типу
void MyApplication::main(std::string /*url*/) 
{ 
response().out() << "<html>\n<body>\n<h1>Hello World</h1>\n</body>\n</html>\n";
}

Адже ми хочемо сайт крутіше, вірно? У CppCMS, щоб отрендерить сторінку, потрібно дві речі — шаблон і пов'язаний з ним контролер (слово «контролер» я став використовувати свавільно, але, як мені здається, воно тут підходить). Шаблон — це файл з вмістом у вигляді суміші HTML і спеціальної мови CppCMS. Контролер — клас (або структура) C++, що містить змінні і функції, на які посилається шаблон. Але менше слів, більше прикладів:
//page.h

namespace Content 
{

struct Page : public cppcms::base_content
{
std::string message;
std::string pageTitle;
};

}

<!-- page.tmlp -->
<% c++ #include "page.h" %>
<% skin my_skin %>
<% view page uses Content::Page %>
<% template render() %>
<html>
<head>
<title><%= pageTitle %></title>
</head>
<body>
<h1><%= message %></h1>
</body>
</html>
<% end template %>
<% end view %>
<% end skin %>

Тут ми бачимо контролер Content::Page з змінними message pageTitle, а також шаблон з ім'ям page, що використовує контролер Content::Page. Можна також задавати різні скіни (skin), але, оскільки я з ними не мав справи, нічого сказати не можу (і взагалі вважаю, що зовнішнім виглядом краще управляти через CSS, хоч ця технологія мені і не подобається).
Зверніть увагу, що заголовковий файл, який відповідає контролеру, повинен бути включений директивою
<% c++ #include "page.h" %>

Ця директива дозволяє вставляти в місці, де вона з'являється, довільний код C++. Для вставки ж значення змінної контролера використовується спеціальна конструкція
<%= variableName %>

Рядок
<% template render() %>

Оголошує функцію, яку потім можна використовувати в різних місцях:
<% template hr() %>
<hr />
<% end template %>

<% template render() %>
<!-- тут код -->
<% include hr() %>
<!-- ще код -->
<% include hr() %>
<!-- і ще -->
<% end template %>

В результаті вміст функції hr з'явиться у всіх місцях, куди вона була включена. Функції можуть містити значну кількість коду, який інакше довелося б копіпаст, що не є добре. Функції також можуть приймати параметри.
Також шаблони підтримують умовні оператори, цикл foreach і ще деякі можливості. Описання цього вистачило б на окрему статтю, тому обмежуся посиланням на документацію і парою зауважень.
По-перше, шаблони можна успадковувати один від одного, перевизначаючи функції (вони завжди оголошуються як віртуальні). Власне, у першому прикладі ми і переопределили функцію render базового шаблону cppcms::base_content.
По-друге, не так щоб зауваження, скоріше маленька підказка: якщо вам треба вивести в шаблоні значення числової змінної, збільшене на якесь число, допоможе ось такий дивний код (інших способів, як я зрозумів, немає):
<% c++ out() << (content.variable + 1); %>

Покажу також, як викликається рендеринг шаблону з додатка:
void MyApplication::handlePage()
{
Content::Page c; //c - тому що controller
c.pageTitle = "My page";
c.message = "Yarrr!";
render("page", c);
}

Всередині функція render рендерить (хто б міг поудмать) шаблон, отримує HTML у вигляді тексту і пише його в response().out().
Ну і наостанок — як запустити генерацію шаблонів (з .pro/.pri файлу):
CPPCMS_PROCESSING_COMMAND=$${CPPCMS_PREFIX}/bin/cppcms_tmpl_cc
mac|unix {
CPPCMS_TEMPLATES=$$files($${PWD}/template/*)
} else:win32 {
CPPCMS_TEMPLATES=$$files($${PWD}\\template\\*)
}

for(CPPCMS_TEMPLATE, CPPCMS_TEMPLATES) {
CPPCMS_TEMPLATES_STRING=$${CPPCMS_TEMPLATES_STRING} \"$${CPPCMS_TEMPLATE}\"
}

CPPCMS_PROCESSING_COMMAND=$${CPPCMS_PROCESSING_COMMAND} $${CPPCMS_TEMPLATES_STRING} -o \"$${PWD}/compiled_templates.cpp\"

win32:CPPCMS_PROCESSING_COMMAND=$$replace(CPPCMS_PROCESSING_COMMAND, "/", "\\")

system(python $${CPPCMS_PROCESSING_COMMAND})

SOURCES += compiled_templates.cpp

Використовується спеціальна утиліта, а також інтерпретатор Python (версія 2.x), отриманий файл включається в проект. Передбачається, що файли шаблонів лежать в папці template і мають розширення .tmpl.

Зберігання

Майже будь-який веб-сайт працює з даними, які треба розміщувати в довготривалій пам'яті (читай — на диску). Зазвичай для цього використовуються реляційні СУБД, такі, як, скажімо, MySQL або SQLite. Дуже зручно використовувати механізм, який би дозволяв прозоро для розробника перетворювати об'єкти мови програмування дані в БД і навпаки. Такий механізм називається Object-relational Mapping (ORM), і в моєму випадку використовується його реалізація у вигляді фреймворку ODB.
Як це виглядає? Наприклад, ось так:
PRAGMA_DB(object table("posts"))
class Post
{
public:
PRAGMA_DB(id auto)
quint64 id_;
PRAGMA_DB(not_null)
QString board_;
PRAGMA_DB(not_null)
quint64 number_;
PRAGMA_DB(not_null)
QDateTime dateTime_;
QString text_;
PRAGMA_DB(not_null)
QLazySharedPointer<Thread> thread_;
public:
explicit Post()
{
//
}
private:
friend class odb::access;
};

Тут ми оголошуємо клас Post, який представляє повідомлення (або пост) на форумі (або дошці). Про те, що цей об'єкт повинен зберігатися в БД і завантажуватися з неї, каже рядок PRAGMA_DB(object table(«posts»)) (є також необов'язковий параметр table(«posts»), явно вказує ім'я таблиці, інакше б використовувалося ім'я за замовчуванням «Post»). Макрос PRAGMA_DB розгортається в #pragma db, далі додаються його аргументи. Макрос використовується, щоб компілятор не видавав попереджень, зустрічаючи незнайомий синтаксис #pragma.
Якщо над змінної класу додати такий же макрос, можна повідомити ODB додаткову інформацію про цієї змінної — те, як вона повинна зберігатися в БД (наприклад, що змінна є ідентифікатором і відповідне поле повинно бути оголошено PRIMARY KEY). На жаль, довільні constraint'и вказати немає можливості, а дуже хотілося б, скажемо, оголосити для таблиці щось на зразок UNIQUE(fieldOne, fieldTwo, fieldThree), тобто вказати унікальність по декільком полям, а не по одному.
Можна в якості типу змінної використовувати інший клас, позначений PRAGMA_DB. Також, як ви помітили, можна вказувати тип змінної класи Qt. Для цього потрібна бібліотека odb-qt. Нарешті, мінлива, чий тип обгорнутий в QLazySharedPointer, не ініціалізується відразу при запиті до БД, а підвантажується окремим запитом пізніше, якщо буде потрібно (Lazy fetch).
Необхідно також оголосити odb::access дружнім класом.
А ось так виглядає збереження і завантаження об'єктів (у разі SQLite):
try {
odb::database *db = new odb::sqlite::database("/path/to/db",
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); //створюємо з'єднання з БД
odb::transaction t = odb::transaction(db.begin()); //починаємо транзакцію
//створюємо схему
t->execute("PRAGMA foreign_keys=OFF");
odb::schema_catalog::create_schema(*db);
t->execute("PRAGMA foreign_keys=ON");
Post p;
//ініціалізуємо змінні поста
db->perist(p); //зберігаємо пост в базі
odb::result<Post> r(db->query<Post>()); //даємо запит список всіх постів
for (odb::result_iterator<Post> i = r.begin(); i != r.end(); ++i) {
//робимо щось з отриманим списком посад, наприклад:
i->dateTime_ = QDateTime::currentDateTimeUtc(); //задаємо час
db->update(*i); //зберігаємо зміни
}
t.commit(); //завершуємо транзакцію
} catch (const odb::exception &e) {
//обробляємо виключення
}

По-хорошому треба ще подбати про видалення вказівника на odb::database, але я не став перевантажувати код (використовуйте для цього scoped pointer).
Варто зауважити, що функція odb::schema_catalog::create_schema не перевіряє, чи є вже таблиці в базі, тобто виконує CREATE TABLE ... замість CREATE TABLE IF NOT EXISTS ..., так що перевіряти потрібно вручну. Природно, ні про яке автоматичному створенні схеми і мови не йде — все вручну. І це ще один камінь в город ODB. Втім, не рахуючи деяких милиць, бібліотека зі своїм завданням справляється.
ODB, як і CppCMS, потрібен свій мета-компілятор, обробний PRAGMA_DB і генерує код C++. Запускається він так:
mac|unix {
ODB_PROCESSING_COMMAND=$${ODB_PREFIX}/bin/odb
ODB_TEMPLATES=$$files($${PWD}/*.h)
} else:win32 {
ODB_PROCESSING_COMMAND=$${ODB_PREFIX}/bin/odb.exe
ODB_TEMPLATES=$$files($${PWD}\\*.h)
}

for(ODB_TEMPLATE, ODB_TEMPLATES) {
ODB_TEMPLATES_STRING=$${ODB_TEMPLATES_STRING} \"$${ODB_TEMPLATE}\"
}

ODB_PROCESSING_COMMAND=$${ODB_PROCESSING_COMMAND} -d sqlite --generate-query --generate-schema --profile qt
ODB_PROCESSING_COMMAND=$${ODB_PROCESSING_COMMAND} -I \"$${QMAKE_INCDIR_QT}\"
ODB_PROCESSING_COMMAND=$${ODB_PROCESSING_COMMAND} -I \"$${QMAKE_INCDIR_QT}/QtCore\" $${ODB_TEMPLATES_STRING}

win32:ODB_PROCESSING_COMMAND=$$replace(ODB_PROCESSING_COMMAND, "/", "\\")

system($${ODB_PROCESSING_COMMAND})

HEADERS += $$files($${PWD}/*.hxx)
SOURCES += $$files($${PWD}/*.cxx)

Компілятор поставляється окремо від бібліотеки у вигляді бінарника. При запуску потрібно вказати список файлів, тип БД (в моєму випадку SQLite), а також те, що використовується профіль Qt і що потрібно згенерувати схему. Плюс вказуються шляхи до заголовочным Qt файлів. Компілятор генерує .hxx і .cxx файли, додаючи суфікс "-odb" до вихідних імен.
Тепер пару слів про моєму проекті. Оскільки ODB вимагає активної транзакції при будь-якій операції, я вирішив обернути зв'язку odb::database odb::transaction в один клас Transaction, який зберігає покажчик на активний в даний момент транзакцію і відповідне їй з'єднання з БД. Одночасно в кожному тред може існувати не більше однієї активної транзакції. При створенні екземпляра класу-обгортки (Transaction), якщо транзакції ще немає, створюється нове з'єднання і починається транзакція, якщо ж активна транзакція вже є, збільшується внутрішній лічильник. При руйнуванні Transaction справжня odb::transaction не буде завершена до тих пір, поки лічильник не обнулиться. Тобто, поки ми не дійдемо до дна стека, де знаходиться створений першим примірник Transaction, ми будемо знаходитися усередині однієї і тієї ж операції, що посилається на одне і те ж з'єднання з базою. Дуже зручно. Джерело тут: 1, 2. Приклади використання: 1, 2.
Для ледачих спрощений приклад під спойлером
bool createPost(CreateThreadParameters &p, QSharedPointer<Thread> thread)
{
try {
Transaction t; //цей об'єкт на вершині стека
Post post(p, thread);
t->persist(post);
t.commit();
return true;
} catch (const odb::exception &e) {
qDebug() << e.what();
return false;
}
}

bool createThread(CreateThreadParameters &p)
{
try {
Transaction t; //цей об'єкт на дні стека
QSharedPointer<Thread> thread(new Thread(p));
t->persist(thread);
if (!createPost(p, thread))
return bfalse;
t.commit();
return true;
} catch (const odb::exception &e) {
qDebug() << e.what();
return false;
}
}

Обидва об'єкти Thread посилаються на одну й ту саму трансакцію і на одне і те ж з'єднання з базою.


Підсвічування синтаксису, перевірка типу файлів

Тут якихось особливих зауважень немає, просто наведу код:
QString mimeType(const QByteArray &data, bool *ok)
{
if (data.isEmpty())
return bRet(ok, false, QString());
magic_t magicMimePredictor;
magicMimePredictor = magic_open(MAGIC_MIME_TYPE);
if (!magicMimePredictor)
return bRet(ok, false, QString());
if (magic_load(magicMimePredictor, 0)) {
magic_close(magicMimePredictor);
return bRet(ok, false, QString());
}
QString result = QString::fromLatin1(magic_buffer(magicMimePredictor, (void *) data.data(), data.size()));
return bRet(ok !result.isEmpty(), result);
}

QString highlight(const QString &code, const QString &lang)
{
std::istringstream in(Tools::toStd(code));
std::ostringstream out;
try {
srchilite::SourceHighlight sourceHighlight("html.outlang");
sourceHighlight.setDataDir("/path/to/definition/files");
sourceHighlight.highlight(in, out, lang.toLatin1().data() + ".lang");
} catch (const srchilite::ParserException &e) {
qDebug() << e.what();
return "";
} catch (const srchilite::IOException &e) {
qDebug() << e.what();
return;
} catch (const std::exception &e) {
qDebug() << e.what();
return;
}
return + QString::fromLocal8Bit(out.str());
}

При визначенні типу файлу саму малість граємося з типом покажчиків, при підсвічування синтаксису вказуємо шлях до папки з файлами визначень синтаксису і мова вихідного коду.

Я хотів би розповісти ще про багато, наприклад, як я реалізовував підтримку різних видів капчі і прикріплення більше одного файлу, обробку статичного вмісту, збереження файлів, кешування… список можна продовжити. Але, боюся, в рамках однієї статті я це зробити не зможу, на жаль мені. Якщо це буде комусь цікаво, то я обов'язково розповім про все перерахованому і багато про що ще у наступних частинах.

Ну а тепер обіцяна диванна аналітика. Спершу, зізнатися, я піддався загальній думці про те, що C++ не підходить для вебу. При цьому я особливо не замислювався, чому саме. Раз вони всі так кажуть, то, напевно, так і є. Це було кілька років тому, коли я поняття не мав, для чого потрібен JavaScrpt, що таке AJAX і чому треба використовувати CSS.
Але минав час, я поколупав Django, Ruby on Rails, попрацював якийсь час з Java, створюючи один великий веб-сайт, набрався досвіду, вивчив нові технології. І я зрозумів, що, насправді, якою б мовою він не використовувався в бэкэнде, фронтенд все одно буде представляти із себе ті ж HTML, CSS і JavaScript. Як не крути, треба писати шаблони сторінок, створювати стилі, програмувати більш складну поведінку на JS. І все це взагалі ніяк не пов'язане з бэкэндом.
Велика різниця між, скажімо, Thymeleaf (Java) і CppCMS при роботі з шаблонами? Не дуже. Все той же мову шаблонів, тільки синтаксис трохи відрізняється. Рендерится ж все точно так само, викликом функції фонового коду. І контролери є і там, і там, не важливо як вони називаються.
А зберігання даних? Чим ODB принципово відрізняється від Hibernate? Так, можливості місцями скромніше, але чи означає це, що ODB зовсім не підходить для ORM? Я так не вважаю.
Ну і так далі. Фронтенд залишається фронтендом, в бэкенде ж ми на будь-якій мові робимо все те ж саме. Так чи є різниця? Виходить, що ні. На Java або Python робота з БД не буде чимось принципово відрізнятися від роботи з БД на C++, це ж стосується і того, що модно називати «бізнес-логікою», то є основною логікою програми. Все ті ж перевірки, умовні оператори, ієрархія класів/функцій, тільки синтаксис у кожного свій мови.
Не вийде, використавши Python, позбутися від необхідності писати на JS AJAX-запити, або перестати звертатися до бази. Таких чудес не буває. Хтось, можливо, скаже, що працювати з БД на %имя_языка% простіше, ніж на C++, і буде частково має рацію, але лише частково: чудес, повторюся, не буває, якщо треба отримати з бази об'єкт, для цього треба написати щось на кшталт Object o = db->query("..."); — будь-якою мовою.
Тобто, виходить, що відповідь на питання «А навіщо?» залишається тим же: «А чому ні?», змінюється лише його зміст. Такі мої спостереження, засновані на особистому досвіді написання веб-додатків на різних мовах (C++, Java, в меншій мірі — Python, Ruby). І це не заклик до холивару, а бажання його мирним чином вирішити.

За сім прощаюся, а також залишаю посилання на джерело проекту: github.com/ololoepepe/ololord
Ну і, зрозуміло, запрошую до обговорення. Чи Писали ви веб-додатків на C++? Чи чули аргументовані обґрунтування, чому це погано? Поділіться своїм досвідом.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.