Another Reflection Library and ORM for C++
Immediately warn of velosipedisti videogo here on display. If reading the title is only with difficulty suppress the exclamation of "fuck, not another
the
why we need another ORM library
When developing 3-tier applications with separate layers of representation (Presentation tier), business logic (Logic tier) and data storage (Data tier) there is invariably the problem of organization of interaction between application components at the interface of these layers. Traditionally, the interface to relational databases is based on SQL queries, but its use directly from business logic layer is usually associated with a number of challenges, some of which can be easily solved by using the ORM (Object-relational mapping):
the
-
the
- the Need to provide entities in two forms: object-oriented and relational the
- the Need to convert between these two forms the
- Susceptibility to errors when manually writing SQL queries (partially can be solved by using various lint tools and plug-ins for modern ides)
The presence of such a simple solution to these problems has led to the emergence of plenty of different ORM implementations for every taste and color (the list is at Wikipedia). Despite the abundance of existing solutions, there is always
Probably such maximalistic thoughts led me, when a couple of years ago I started writing an ORM for your needs. Briefly still describe what was wrong with the ORM, which I tried and that I wanted them to fix it.
-
the
- first is the need for static typing, which would allow to catch most of the errors when writing queries to the database during compile time, and therefore would greatly accelerated the development speed.
Condition for implementation: this should be a reasonable compromise between the level of test queries, a compile-time (in the case of C++ also associated with responsiveness of the IDE) and readability of code.
the - second is flexibility, the ability to write any (reasonable) requests. In practice, this item is reduced to the possibility of writing, SUPO (create-delete-get-update) request from any WHERE-subexpressions and the ability to run cross-table queries. the
- followed by support for DBMS from different vendors at "the program should continue to work correctly when you leap from one DBMS to another." the
- Possibility of reusing the ORM of reflection for other purposes (serialization, script binding, factories decoupled from the implementation, etc.). What can we say, often a reflection of the existing decisions "nailed" to ORM. the
- still do not want to depend on code generators a La Qt's moc, protoc, thrift. So let's try to get by means of C++ templates and preprocessor C.
the
the Actual implementation
Consider it for a "toy" example from the tutorial SQL. Have 2 tables: Customer and order are related to each other by one-to-many relationship.
In the code declaring the class in the header as follows:
the
// Declare object-relational
struct Customer : public Object
{
uint64_t id;
String first_name;
String second_name;
Nullable<String> middle_name;
Nullable<DateTime> birthday;
bool news_subscription;
META_INFO_DECLARE(Customer)
};
struct Booking : public Object
{
uint64_t id;
uint64_t customer_id;
String title;
uint64_t price;
double quantity;
META_INFO_DECLARE(Booking)
};
As you can see, these classes are inherited from a common ancestor Object (why to be original?), in addition to the method declarations contains a macro META_INFO_DECLARE. This method simply adds the Declaration of overloaded and overridden methods of Object. Some of the fields declared using the Nullable wrapper, it is not difficult to guess, these fields can accept NULL. All field columns must be public.
The definition of classes is somewhat more monstrous:
the
STRUCT_INFO_BEGIN(Customer)
FIELD(Customer id)
FIELD(Customer, first_name)
FIELD(Customer, second_name)
FIELD(Customer, middle_name)
FIELD(Customer birthday)
FIELD(Customer news_subscription, false)
STRUCT_INFO_END(Customer)
REFLECTIBLE_F(Customer)
META_INFO(Customer)
DEFINE_STORABLE(Customer,
PRIMARY_KEY(COL(Customer::id)),
CHECK(COL(Customer::birthday), COL(Customer::birthday) < DateTime(1998, January, 1))
)
STRUCT_INFO_BEGIN(Booking)
FIELD(Booking id)
FIELD(Booking, customer_id)
FIELD(Booking, title, "noname")
FIELD(Booking, price)
FIELD(order quantity)
STRUCT_INFO_END(Booking)
REFLECTIBLE_F(Booking)
META_INFO(Booking)
DEFINE_STORABLE(Booking,
PRIMARY_KEY(COL(Booking::id)),
INDEX(COL(Booking::customer_id)),
// N-to-1 relation
REFERENCES(COL(Booking::customer_id), COL(Customer::id))
)
Unit STRUCT_INFO_BEGIN...STRUCT_INFO_END creates descriptor definition of reflection class fields. Macro REFLECTIBLE_F creates a class specifier for fields (there are REFLECTIBLE_M, REFLECTIBLE_FM to create descriptors of classes supporting reflection methods, but do not post about it). The macro creates META_INFO define overloaded methods Object. And finally, the most interesting macro DEFINE_STORABLE creates a relational table definition on the basis of reflection of the class and announced restrictions (constraints) that ensure the integrity of our scheme. In particular, check the one-to-many relationship between the tables and check on the birthday field (just for example, we want to serve only adult customers). Create the necessary tables in the database is simple:
the
SqlTransaction transaction;
Storable<Customer>::createSchema(transaction);
Storable<Booking>::createSchema(transaction);
transaction.commit();
SqlTransaction is not difficult to guess, provides isolation and atomicity of operations performed, and also grabs the database connection (can be multiple named connections to different DBMS, or parallelization of queries to one database Connection Pooling). In this regard, you should avoid recursive installierbare transaction — you can get a Dead Lock. All requests must be made in the context of some transaction.
the
Queries
sample queries
This is the easiest type of queries. Just prepare our object and call the method insertOne on it:
the
You can also one to add to the database multiple records (Batch Insert). In this case, the query is prepared only once:
the
Getting data from database in the General case is as follows:
the
In this case, the pagination of all orders Ivanov. Alternatively, the receipt of all
table entries list:
the
One scenario: update the record just retrieved from the database by primary key:
the
Alternative you can generate the request manually:
the
Similarly, with the update-query you can delete a record by primary key:
the
Or using the query:
the
INSERT
This is the easiest type of queries. Just prepare our object and call the method insertOne on it:
the
SqlTransaction transaction;
Storable<Customer> customer;
customer.init();
customer.first_name = "Ivan";
customer.second_name = "username";
customer.insertOne(transaction);
Storable<Booking> booking;
booking.customer_id = customer.id;
booking.price = 1000;
booking.quantity = 2.0;
booking.insertOne(transaction);
transaction.commit();
You can also one to add to the database multiple records (Batch Insert). In this case, the query is prepared only once:
the
Array<Customer> customers;
// filling the array with customers
SqlTransaction transaction;
Storable<Customer>::insertAll(transaction customers);
transaction.commit();
SELECT
Getting data from database in the General case is as follows:
the
const int itemsOnPage = 10;
Storable<Booking> booking;
SqlResultSet resultSet = booking.select().innerJoin<Customer>()
.where(COL(Customer::id) == COL(Booking::customer_id) &&
COL(Customer::second_name) == String("username"))
.offset(page * itemsOnPage).limit(itemsOnPage)
.orderAsc(COL(Customer::second_name), COL(Customer::first_name))
.orderDesc(COL(Booking::id)).exec(transaction);
// Forward iteration
for (auto&row : resultSet)
{
std::cout << "Booking id:" << booking.id << ", title: "<< booking.title << std::endl;
In this case, the pagination of all orders Ivanov. Alternatively, the receipt of all
table entries list:
the
auto customers = Storable<Customer>::fetchAll(transaction,
COL(Customer::birthday) == db::null);
for (auto& customer : customers)
{
std::cout << customer.first_name << "" << customer.second_name << std::endl;
}
UPDATE
One scenario: update the record just retrieved from the database by primary key:
the
Storable<Customer> customer;
auto resultSet = customer.select()
.where(COL(Customer::birthday) == db::null)
.exec(transaction);
for (auto row : resultSet)
{
customer.birthday = DateTime::now();
customer.updateOne(transaction);
}
transaction.commit();
Alternative you can generate the request manually:
the
Storable<Booking> booking;
booking.update()
.ref<Customer>()
.set(COL(Booking::title) = "All sold out",
COL(Booking::price) = 0)
.where(COL(Booking::customer_id) == COL(Customer::id) &&
COL(Booking::title) == String("noname") &&
COL(Customer::first_name) == String("username"))
.exec(transaction);
transaction.commit();
DELETE
Similarly, with the update-query you can delete a record by primary key:
the
Storable<Customer> customer;
auto resultSet = customer.select()
.where(COL(Customer::birthday) == db::null)
.exec(transaction);
for (auto row : resultSet)
{
customer.removeOne(transaction);
}
transaction.commit();
Or using the query:
the
Storable<Booking> booking;
booking.remove()
.ref<Customer>()
.where(COL(Booking::customer_id) == COL(Customer::id) &&
COL(Customer::second_name) == String("username"))
.exec(transaction);
transaction.commit();
The main thing you need to pay attention to, where a subquery is a C++ expression, on the basis of which to build an abstract syntax tree (AST). The tree transformirovalsya in the SQL expression to a specific syntax. It just provided static typing I mentioned in the beginning. Intermediate form of the query in the form of the AST allows us to describe a unified query regardless of the DBMS vendor, I had to spend some amount of effort. The current version supports PostgreSQL, MariaDB and SQLite3. On vanilla MySQL, too, should in principle start the engine, but this engine otherwise handle certain types of data, respectively, the proportion of tests it fails.
the
What else
You can describe the custom stored procedure and use them in queries. ORM now supports some built-in functions DBMS out of the box (upper, lower, ltrim, rtrim, random, abs, coalesce, etc.), but you can define your own. So, for example, describes the function strftime in SQLite:
the
namespace sqlite {
inline ExpressionNodeFunctionCall<String> strftime(const String&fmt, const ExpressionNode<DateTime>& dt)
{
return ExpressionNodeFunctionCall<String>("strftime", fmt, dt);
}
}
In addition, the ORM implementation is not limited to the possible use of reflection. It seems that the correct reflection we will get in C++ (correct reflection needs to be static, i.e. enforced at the compiler level rather than the library) so you can try to use this ralizatsii for serialization and integration with scripting engines. But I maybe will write another time, if anyone is interested.
the
What is not
The main drawback in the module SQL, I could not support aggregate queries (count, max, min) and grouping (group by). Also, the list of supported DBMS is quite poor. Perhaps in the future it will support SQL Server via ODBC.
Also, any thoughts on integrating with mongodb, especially since the library allows to describe and "non-planar" structure (with substructures and arrays).
Link repository.
Комментарии
Отправить комментарий