Комментарии 57
в нынешней версии библиотеки связей нет. Вообще уже был запрос один добавить foreign key
. В целом это сделать несложно, вопрос в другом — нужно ли это во встраиваемой БД. Лично я не использую связи даже при их логическом наличии, так как это создает runtime overhead. Однако спасибо за отзыв — в дальнейших версиях обязательно будет добавлена функция foreing_key
со ссылкой на поле другого класса.
Просто я тоже искал «идеальную» ORM для с++, но ничего «годного» не нашел. Вы пытаетесь сделать решение, лучшее, чем существующие, и в этом случае без этих фич никуда. Попробовав, например, hibernate для java или sequelize для node.js вряд ли Вы будете делать запросы руками, которые за Вас может сделать ORM, даже если это просто. Я бы с радостью перевел некоторые свои поделки на с++, как на свой первый и «родной» язык, но пока все ORM, на которые я смотрел имеют только лишь недостатки. Select у Вас выглядит, действительно, очень хорошо, но, согласитесь, достаточно узкая специализация получилась, без join'ов, и, как уже указано leschenko group by и select field1, field2… а не select * (и, кстати, offset'а)
Расскажите, пожалуйста, поподробнее про "возможность достать связанные foreign keyем сущности". Я так понимаю это что-то типа взяли юзера, у него есть поле category_id
, достали категорию с этим id?
Причем обычно это можно делать как с помощью +1 запроса, так и через JOIN.
Book.findAll({
limit: 10,
order: [['createdAt', 'DESC']],
include: [{
model: Category,
as: 'categories',
limit: 3,
where: {
parentId: null
},
attributes: ['id', 'title'] //Зачем получать лишнюю информацию из базы?
}]
});
В вашем примере есть вложенные сущности, а они не поддерживаются в sqlite_orm
просто потому что это усложняет основу системы. Более того, взаимновложенные сущности не скомпилируются в С++ вообще (при условии агрегации).
А то, о чем вы говорите, реализуется в sqlite_orm
вот так:
struct Book {
int id;
std::string name;
int categoryId;
int createdAt;
};
struct Category {
int id;
std::string name;
};
using namespace sqlite_orm;
auto storage = make_storage("db.sqlite",
make_table("books",
make_column("id", &Book::id, primary_key()),
make_column("name", &Book::name),
make_column("category_id", &Book::categoryId),
make_column("created_at", &Book::createdAt)),
make_table("categories",
make_column("id", &Category::id, primary_key()),
make_column("name", &Category::name)));
// лимита нет, каюсь, в ближайших коммитах будет
auto books = storage.get_all<Book>(order_by(&Book::createdAt));
// теперь вытащим вектор из id категорий
std::vector<decltype(Book::categoryId)> categoryIds;
categoryIds.reserve(books.size());
std::transform(books.begin();
books.end();
std::back_inserter(categoryIds),
[](auto &book) {
return book.categoryId;
});
// std::transform может быть неудобен, но это православная stl-реализация функции map
auto categories = storage.get_all<Category>(where(in(&Category::id, categoryIds)));
// теперь books содержит выбранные книги, а categories - их категории
Или можно сымитировать сложные запрос:
auto books = storage.get_all<Book>(order_by(&Book::createdAt));
auto categories = storage.get_all<Category>(where(in(&Category::id,
storage.select(&Book::categoryId)
)));
Я понимаю, что пока Вы такие случаи не рассматриваете, но они встречаются очень часто в реальной жизни. Как ORM для несвязанных между собой таблиц получилось неплохо, и, надеюсь, полностью покрывает ваши нужды ;)
расскажите поподробнее. Изначально цель sqlite_orm
покрыть бОльшую часть использования sqlite3 в приложениях, а значит ваш кейс особенно интересен. Покажите схему как хотите хранить отношения многие ко многим. Отдельным словарем или как?
Вы все сами уже ниже описали ;)
CREATE TABLE books(id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE categories(id INTEGER PRIMARY KEY, name TEXT)
CREATE TABLE books_categories(category_id INTEGER, book_id INTEGER)
Ничего хитрее я не имел в виду
Понял, в каком финальном виде вы хотите получить данные?
struct Category;
struct Book {
int id;
std::string name;
int createdAt;
std::vector<Category> categories;
};
struct Category {
int id;
std::string name;
std::vector<Book> books;
};
И что-нибудь типа такого в описании:
make_table("books", make_many_to_many_association("categories", &Book::categories, &Category::books, through(&BookCategories)));
Тут скорее всего надо еще руками определить `BookCategories` и привязывать не к `&Category::books` а к `&BookCategories::bookId`, но в идеале это делать автоматически.
Пишу «типа такого», потому что если бы у меня было четкое представление как это сделать, я бы это давно уже сделал ;)
Идея отличная, только структуры схеме не соответствуют. Это создает неочевидности. Я не говорю, что вы что-то говорите неверно, я пытаюсь понять как лучше добавить возможность вложенных сущностей.
Вообще при схеме (добавил NOT NULL так как зануляемые значения мапятся в указатели только)
CREATE TABLE books(id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE categories(id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL)
CREATE TABLE books_categories(category_id INTEGER NOT NULL, book_id INTEGER NOT NULL)
Достать все категории конкретной книги можно так:
struct Book {
int id;
std::string name;
};
struct Category {
int id;
std::string name;
};
struct BookCategory {
int categoryId;
int bookId;
};
using namespace sqlite_orm;
auto storage = make_storage("db.sqlite",
make_table("books",
make_column("id", &Book::id, primary_key()),
make_column("name", &Book::name)),
make_table("categories",
make_column("id", &Category::id, primary_key()),
make_column("name", &Category::name)),
make_table("books_categories",
make_column("category_id", &BookCategory::categoryId),
make_column("book_id", &BookCategory::bookId)));
auto book = storage.get<Book>(2);
auto categories = storage.get_all<Category>(where(in(&Category::id,
storage.select(&BookCategory::categoryId, where(is_equal(&BookCategory::bookId, book.id)))
)));
Разумеется, так можно. И нужно делать именно так, если ORM не поддерживает вложенные сущности. Но, согласитесь, хочется спихнуть весь этот (можно сказать служебный и практически ненужный) код на ORM
структуры схеме не соответствуют
Все правильно, они и не должны. Если я хочу добавить transient поле в структуру, я его туда добавлю, но не буду добавлять в make_column. Ну и по-хорошему в структуре пометить их комментарием, что они не сериализуются.
Здесь еще тонкий момент со связями не многие-ко-многим. Придется писать в структуре или experimental::optional<T>
или, как в вашем решении std::shared_ptr<T>
, что, безусловно, снизит читаемость кода
Все правильно, они и не должны.
Что?
sqlite_orm
как и любая другая sqlite ORM библиотека отправляет запросы в первую очередь в SQLite. И разработчик работая с ORM или еще каким-угодно фронтэндом БД должен понимать в какие запросы его клиентский код вырождается в результате. Так вот: когда классы соответствуют схеме — это делает код очень читабельным в плане "где и какой sql запрос отправляется". Иначе повышается шанс быдлокодинга, так как новичок может не понимать какие запросы под собой подразумевает вот эта простая и удобная в использовании функция. Именно поэтому последний пример
auto categories = storage.get_all<Category>(where(in(&Category::id,
storage.select(&BookCategory::categoryId, where(is_equal(&BookCategory::bookId, book.id)))
)));
идеально иллюстрирует вложенный запрос:
SELECT * FROM categories WHERE id IN (
SELECT category_id FROM books_categories WHERE book_id = ?
)
Второе: поддержки std::experimental::optional<T>
в sqlite_orm
нет пока он не выйдет из experimental
namespace (ожидается в С++17). Опциональные значения используют std::shared_ptr<T>
либо std::unique_ptr<T>
. Это неудобно, согласен. Я думал добавить сырые С-указатели, но этого не будет — вместо этого я добавлю трэйты чтобы можно было подключать свои типы полей — собственные строки, собственные опционалы. Там уже разработчик если хочет — добавит С-указатели или Glib::ustring
, QString
etc.
А вообще у меня мысли такие: сделать возможность хранить вложенную сущность (пока только в одном экземпляре) в виде поля типа std::shared_ptr<T>
где T это другой класс также связанный с хранилищем. Насчет векторов тут надо подумать. Ваш совет про make_many_to_many_association
очень интересен
когда классы соответствуют схеме
Я хотел лишь сказать, что если я захочу добавить в структуру какое-то поле, например, немного надуманное поле shouldHavePassport
пользователю, которое не хранится в БД, а вычисляется в триггере или еще где-нибудь потом как user.age >= 14
или еще и учитывает страну, то будет нелогично не добавлять его в структуру. Его стоит указать несериализуемым, да, но добавлять в структуру просто необходимо, в то время как к базе оно может не иметь никакого отношения.
идеально иллюстрирует вложенный запрос
О, спасибо, теперь я понял что мне напомнила ваша ORM. Текстовое представление regexp'ов
Я думал добавить сырые С-указатели, но этого не будет
Правильно, не надо, мы живем в С++1(4|7)
тут надо подумать
Тут думать не передумать. Посмотрите в строну sequelize. К сожалению, не все их решения можно перевести на С++, но как позаимствовать идеи, синтаксис и некоторые фичи — очень полезно будет
Добавить поля, которые не сериализуются, в структуру не составляет проблем. Это даже подразумевается либой. Просто не создавайте колонку при создании хранилища со ссылкой на это поле и все
Я так и понял, я не понял к чему тогда относился комментарий
структуры схеме не соответствуют
Тут другой случай — тут не просто абы-какие поля, о которых хранилище не знает, тут поля, типы которых связаны с хранилищем, но не в том виде, в котором они оформлены внутри структур. Это порождает несоответствие схемы в бд схеме доменной модели в программе и делает код менее читабельным, и, как следствие, менее поддерживаемым. А если у меня просто будет лишнее поле, которое не нужно сериализоваться (хэш имени пользователя, например), то это не породит неясностей, так как ясно, что это приблуда
А, Вы про то, что в базе написано categoryId
, а в структуре category
, например?
да
В таком случае не знаю, у меня никогда такая трактовка не вызывала какого-то непонимания. Если указан примитивный тип — поле, хранимое в БД; не примитивный тип T
— связь, когда на другом конце один объект; std::vector<T>
— связь, когда эта сущность имеет много других объектов. В таком случае даже не надо думать, как это хранится в БД — через третью таблицу или через строку с id через запятую, если утрировать. Мы просто знаем, что это сущность связана с другой, а имя поля должно однозначно говорить как и зачем.
окей, тогда мне следует добавить поддержку этого в sqlite_orm
. И если один ко многих ясно как сделать, то вектор устроен хитрее — тут надо умно и очевидно указывать какая таблица выполняет роль словаря (в нашем случае это books_categories
)
Да, именно. В таком случае Вы сможете в промежуточной таблице также хранить какую-нибудь информацию о этой связи. Это, конечно, подбросит Вам еще "парочку" подводных камней ;)
@igorjan94 я понял, теперь я нахожусь в процессе осознания, что вложенные сущности нужны. Но вот такой вопрос от меня: в случае если есть такая схема (буду писать коротко):
books ( id:int, name:string, category_id:int )
categories ( id:int, name:string )
то подразумевается, что получая книгу, в ней есть вложенная категория с указанным id. То есть, ORM за юзера выполняет каскадный SELECT
и достает нужную категорию. Это я понял. Но вот вопрос: а что если ссылки взаимные, то есть у категории есть id книги, которую добавили самой последней в эту категорию, например. Так:
categories ( id:int, name:string, last_added_book_id:int? )
Тут может быть такая ситуация, когда книга ссылается на категорию, которая ссылается на эту же книгу, которая ссылается на эту же категорию. В данном случае что делает ORM (например, та сама на js, которую, кажется, вы мне рекомендовали как пример)? Попадает в бесконечную рекурсию?
В данном конкретном случае все немного сложнее. Sequelize попадет в рекурсию на моменте создания сущностей. Точнее по какой-то причине они не "создали таблицы, затем alter table'ом добавили foreign key", а по моему личному ощущению примерно так: "создадим таблицы в порядке, обратному топологической сортировки". Это касается исключительно ограничений на foreign key, при выключении которых все замечательно заводится.
Если же говорить про сам запрос, то нет, не попадет. Ведь Вы в запросе явно или неявно указываете, что нужно доставать из базы
Book.findAll({
include: [{
model: Category,
as: 'category',
include: [{
model: Book,
as: 'last_added_book',
include: [{
model: Category,
as: 'category'
}]
}]
}]
});
В рекурсию может попасть сериализатор, если сущность хранится как ссылка на объект из базы. Но с этим в sequelize я не встречался, встречался только при использовании кривыми руками java+hibernate+jackson
А есть пример как такой набор будет выглядеть? У меня сильные подозрения что при вводе лимита ОРМ будет создать подзапрос на каждую книгу.
Оказывается, в sequelize нельзя указывать limit при many-to-many(мне не нужно было), хотя может быть в версии 4.0 уже и можно
SELECT
`Book`.*,
`categories`.`id` AS `categories.id`,
`categories`.`name` AS `categories.name`,
`categories.BookCategories`.`bookId` AS `categories.BookCategories.bookId`,
`categories.BookCategories`.`categoryId` AS `categories.BookCategories.categoryId`
FROM
(SELECT
`Book`.`id`,
`Book`.`title`
FROM
`Books` AS `Book`
WHERE
(SELECT
`BookCategories`.`bookId`
FROM
`BookCategories` AS `BookCategories`
INNER JOIN `Categories` AS `Category` ON `BookCategories`.`categoryId` = `Category`.`id`
WHERE
(`Book`.`id` = `BookCategories`.`bookId`)
LIMIT 1) IS NOT NULL
ORDER BY `Book`.`createdAt` DESC
LIMIT 10) AS `Book`
INNER JOIN
(`BookCategories` AS `categories.BookCategories`
INNER JOIN `Categories` AS `categories` ON `categories`.`id` = `categories.BookCategories`.`categoryId`) ON `Book`.`id` = `categories.BookCategories`.`bookId`
AND `categories`.`parentId` IS NULL
ORDER BY `Book`.`createdAt` DESC;
Вообще, я считаю, что если кто-то использует ORM, то он должен понимать, что в такого вида запросах ORM скорее всего сгенерирует неэффективный код. Но зато это будет работать сразу и как ожидается
Можно ли вытянуть не все колонки, а только заданные? Т.е. могу ли я достать из базы только id,name от юзера? Я вот хочу аватарку хранить в базе, но вытягивать ее для отображения простого списка (где нет аватарки) иногда не хочу.
Без этого ORM это «неполноценный» ORM.
select
присутствует для одной колонки только пока. В ближайших коммитах будет интерфейс для выбора нескольких колонок в std::tuple
: auto vectorOfTuples = storage.select(columns(&User::id, &User::firstName));
.
Насчет JOIN
и GROUP BY
покажите примеры из своей практики чтобы я понял вас лучше. Спасибо
Необходимо:
1. при отображении списка категорий, отображать количество книг в ней — нужен group by.
2. отображить список книг, выбранной категории — нужен join.
Можно обойтись и без этого, но с проседанием производительности и большим расходом памяти.
CREATE TABLE books(id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE categories(id INTEGER, name TEXT)
CREATE TABLE books_categories(category_id INTEGER, book_id INTEGER)
Вы подразумеваете такую схему? И напишите пожалуйста запрос каким бы вы достали данные для пунктов 1 и 2. Спасибо
select c.name, case when t.cnt is null then 0 else t.cnt end from categories as c
left outer join (
select link.category_id as id, count(*) as cnt from books_categories as link
group by link.category_id
) as t on c.id = t.id
и
select book.* from books_categories as link
inner join books as book on link.book_id = book.id
where link.category_id=@categoryId
Очень интересный кейс. Чтобы его воспроизвести в sqlite_orm
нужно время. А пока в последнем коммите добавлена возможность выбора нескольких колонок как вы и говорили. В конце статьи добавлен пример
проверьте в нынешней версии — теперь есть и GROUP BY, и JOIN (все виды кроме NATURAL и SELF)
разумное замечание. Сейчас я размышляю сделать так: оставить get_all
, добавить auto it = storage.iterate<User>().begin();
. Итератор будет хранить в себе statement указатель, оператор* будет возвращать объект, оператор++ будет двигать statement вперед, storage.iterate<T>.end()
будет возвращать итератор с нулевым statement. При присвоении итератору .end() или при очередном оператор++ когда достигнут конец таблицы итератор закрывает statement. В общем, получится forward iterator полностью stl-совместимый. Тут, конечно, есть косяк, что итератор хранит важную инфу, что не совсем stl-style, но с другой стороны можно будет делать так:
for(auto &user : storage.iterate<User>()) {
if(user.id == idBeingSearch) break;
}
И после break statement закроется, что по сути и есть то, о чем вы говорите — одномоментно в памяти висит только один юзер, а не все сразу.
пунктом про single responsibility
я не спорю, что pragma это очень важный и нужный инструмент. То же самое можно сказать даже про макросы. Однако речь не об этом, а о том, что при подключении ORM приходится модифицировать код модели. Вот тут https://habrahabr.ru/post/263027/ очень хорошо описано почему так делать не надо
struct User{
int id;
std::string firstName;
std::string lastName;
int birthDate;
std::shared_ptr<std::string> imageUrl;
int typeId;
};
#pragma db object(User)
#pragma db member(User::id) id
#pragma db member(User::firstName)
#pragma db member(User::lastName)
#pragma db member(User::birthDate)
#pragma db member(User::imageUrl);
#pragma db member(User::typeId)
struct UserType {
int id;
std::string name;
};
#pragma db object(UserType)
#pragma db member(UserType::id) id
#pragma db member(UserType::name)
/*
Можно даже так сделать:
#ifdef ODB_COMPILER
# include "user-pragmas.hxx"
#endif
или из командной строки:
--odb-epilogue '#include "user-pragmas.hxx"'
*/
По правде сказать из всех ORM-библиотек, которые я видел, ODB меня впечатлила больше всего. У меня вопрос: кто именно занимается парсингом прагм? Тут, похоже, есть кастомный предкомпилятор как у Qt (MOC) или у IOD (https://github.com/matt-42/iod) парсер символов
Пожалуйста. Впервые с IOD я столкнулся вот тут http://siliconframework.org. Может этот фреймворк будет вам тоже интересен.
Насчет ODB — классная штука за одним исключением — предкомпилятором. С++ и без того получает много критики за наличие сишного препроцессора. А если я подключу либу с предпредкомпилятором, что тогда получится? А если у меня две? Три? Кто первичен? Кто вторичен? Почему именно так? Например, у меня проект на Qt (у него есть MOC), я подключил ODB (у него своя приблуда), и еще IOD добавил, у него свой скрипт, который генерит заголовок с символами. Это все порождает путаницу и выходит за пределы возможностей] языка. Именно поэтому я такую черную магию не одобряю. Это мое личное мнение. Не сомневаюсь, что те два ваших проекта, которые содержали ODB, отлично функционировали в области взаимодействия с БД.
Посмотрев внимательнее ODB
я нашел еще одну важную вещь, которая есть в sqlite_orm
и отсутствует в ODB
— это возможность именовать столбцы таблиц независимо от имен полей класса. Как бы это смешно не звучало, но если в схеме есть колонка "new", например, то в C++ такой код не скомпилируется. Самое забавное, что если сменить язык на тот, в котором слово "new" не зарезервировано, то все заработает. Я сталкивался с таким лично в разработке (правда, это был enum-маппер, либа https://github.com/aantron/better-enums). Уговорить бэкенд разраба чтобы он поменял имя колонки в таблице только потому что это слово зарезервировано языком, на котором написан один из клиентов, нереально, да и идиоматически это неверно, на моя взгляд.
Но если использовать так
struct User{
int id;
int birthDate;
char firstName[64];
char lastName[64];
};
то прослойка ORM вроде и не нужна, можно использовать fopen/fwrite.
Понимаете, к чему это?
C++ располагает к использованию UserType typeId; вместо int userTypeId; т.е. к использованию иерархического дерева объектов вместо таблиц.
нет, не понял ни первое, ни второе. 1) почему прослойка не нужна при использовании статичных С-массивов? 2) "С++ располагает к использованию UserType typeId; вместо int userTypeId; т.е. к использованию иерархического дерева объектов вместо таблиц." — вы про вложенные сущности? Если да, то вложенные сущности это ад. Это понимаешь, когда пытаешься масштабировать систему. Вложенные сущности актуальны в проектах с небольшим сроком жизни. Я не спорю — таких очень много, поэтому на вложенные сущности много кто смотрит как на нечто удобное. Плюс в таком виде как вы показали вложенные сущности не скомпилируются если вложение взаимное. Если я вас понял неверно — поясните пожалуйста.
auto user = storage.get<User>(123);
auto types = storage.get_all<UserType>();
...
auto iter = std::find_if( std::begin(type), std::end(type), [&user](const UserType & t){ return t.id == user.typeId; } );
assert(iter != std::end(type));
const UserType & ut = *iter;
где обычно ожидается user.type.
Вот так это делается
auto user = storage.get<User>(123);
auto userType = storage.get<UserType>(user.typeId);
Точнее "таблично-ориентированность" называется отсутствием вложенных сущностей.
По правде сказать вы уже не первый кто говорит про это, и я задумался как можно вложенность реализовать в sqlite_orm
Как в традиционную схему приложения на QtWidgets, что создаёт QtCreator, вписать вот это
auto storage =
Чтобы потом видеть этот storage в пределах всех функций главной формы?
И возможно ли использовать sqlite_orm параллельно с QSqlDatabase, не закрывая попеременно в них соединения?
Не сочтите за рекламу, первое видео с примерным описанием используемого мною решения.
я так понял ваш вопрос состоит в том, чтобы как-то вывести тип хранилища. Это очень популярный вопрос у пользователей sqlite_orm
, и ответ на него я подробно описал в FAQ на github вот тут https://github.com/fnc12/sqlite_orm/wiki/FAQ
Новая SQLite ORM для C++