数据库
现在阅读
C++ Object Relational Mapping (ORM)- Eating the Bun – Part 1 of N
0

C++ Object Relational Mapping (ORM)- Eating the Bun – Part 1 of N

由 ultracpy2018年1月26日

Introduction

Object Relational Mapping is the process of mapping data types between an object oriented language as C++ to a relational type system as SQL. So what is the challenge? C++ has different types like primitive types as int, char, float, double and variations of that. So it’s a real challenge to map all these to an actual SQL type. There may be or may not be an exact type that is similar to the C++ types. Say for float, C++ and SQL may support different kind of standards too. So there are different tools to do this job. There are a lot of matured libraries too out there in market. ODB is one that is really nice.

To help me in my daily work, I have created a simple C++ library called as Bun.

Features

  • Easy to use
  • Object persistence
  • EDSL Object Query Language (No SQL Query needed)
  • Compile time EDSL syntax check for type safety
  • Embedded database

Background

In a lot of my tools application, I use Sqlite as the primary db. Every time I use SQL queries, I feel like wasting a lot of energy in the task that are not really related to my actual usecase. So I thought of creating a framework for the automated mapping of these types. The criteria for the library is as follows:

  1. Free to use for any kind of project
  2. Easy to use
  3. No SQL queries needed. EDSL query.
  4. Expressive
  5. Should be DSL for C++ so the queries syntax can be checked by the C++ compiler
  6. No customization compiler needed (C++11 or more)
  7. Performant

All of these haven’t been met till now. Going on eventually, I will be addressing all these issues. Currently, only a basic version of the library has been developed.

Using the Code

Before we get into the gory details of the internals, in this first article, let’s see how to use the library.

Bun is having a BSD 3-Cluase License. It depends on the following opensource and free libraries:

  1. boost (I have tested on 1.61 version, Boost License)
  2. fmt (Small, safe and fast formatting library, BSD License)
  3. spdlog (Fast C++ logging, MIT License)
  4. Sqlite (Self-contained, serverless, zero-configuration, transactional SQL database engine, Public domain)
  5. sqlite_modern_cpp (C+ wrapper over Sqlite, MIT License)

The github page contains all the dependencies needed. It contains a Visual Studio 2015 solution file also for ease of use. Only boost is not included. So download the project, put the boost headers under the “include” directory or change the solution file path in the solution file.

#include "blib/bun/Bun.hpp"

namespace test {
  // Class that needs to be persisted
  struct Person {
    std::string name;
    int age;
    float height;
  };
}

/////////////////////////////////////////////////
/// Generate the database bindings at compile time.
/////////////////////////////////////////////////
GENERATE_BINDING( (test::Person, name, age, height) );

int main() {
  namespace bun = blib::bun;
  namespace query = blib::bun::query;

  // Connect the db. If the db is not there it will be created.
  // It should include the whole path
  bun::dbConnect( "test.db" );
  // Create the schema. We can create the schema multile times. If its already created
  // it will be safely ignored
  bun::createSchema<test::Person>();

  // Creat some entries in the database
  for (int i = 1; i < 1000; ++i) {
    // PRef is a reference to the persistant object.
    // PRef keeps the ownership of the memory. Release the memory when it is destroyed.
    // Internally it holds the object in a unique_ptr
    // PRef also has a oid associated with the object
    bun::PRef<test::Person> p = new test::Person;

    // Assign the members values
    p->age = i + 10;
    p->height = 5.6;
    p->name = fmt::format( "Brainless_{}", i );
    // Persist the object and get a oid for the persisted object.
    const bun::SimpleOID oid = p.persist();

    //Getting the object from db using oid.
    bun::PRef<test::Person> p1( oid );
  }

  // To get all the object oids of a particular object.
  // person_oids is a vector of type std::vector<blib::bun<>SimpleOID<test::Person>>
  const auto person_oids = bun::getAllOids<test::Person>();

  // To get the objects of a particular type
  // std::vector<blib::bun::Pref<test::Person>>
  const auto person_objs = bun::getAllObjects<test::Person>();

  // EDSL QUERY LANGUAGE ----------------------
  // Powerful EDSL object query syntax that is checked for syntax at compile time.
  // The compilation fails at the compile time with a message "Syntax error in Bun Query"
  using PersonFields = query::F<test::Person>;
  using FromPerson = query::From<test::Person>;
  FromPerson fromPerson;
  // Grammar are checked for validity of syntax at compile time itself.
  // Currently only &&, ||, <, <=, >, >=, ==, != are supported. They have their respective meaning
  // Below is a valid query grammar
  auto valid_query = PersonFields::age > 10 && PersonFields::name != "Brainless_0";
  std::cout << "Valid Grammar?: " << query::IsValidQuery<decltype(valid_query)>::value << std::endl;

  // Oops + is not a valid grammar
  auto invalid_query = PersonFields::age + 10 && PersonFields::name != "Brainless_0";
  std::cout << "Valid Grammar?: " << query::IsValidQuery<decltype(invalid_query)>::value << std::endl;

  // Now let us execute the query.
  // The where function also checks for the validity of the query, and fails at compile time
  const auto objs = fromPerson.where( valid_query ).where( valid_query ).objects();
  // Can even use following way of query
  // As you see we can join queries 
  const auto q = PersonFields::age > 21 && PersonFields::name == "test";
  const auto objs_again = FromPerson().where( q ).objects();
  const auto objs_again_q = FromPerson().where( PersonFields::age > 21 && PersonFields::name == "test" ).objects()
  // Not going to compile if you enable the below line. Will get the "Syntax error in Bun Query" compile time message.
  //const auto objs1 = FromPerson.where( invalid_query ).objects();

  // Check the query generated. It does not give the sql query.
  std::cout << fromPerson.query() << std::endl;

  return 0;
}

So this is how we persist the object. After running this, the following list is created in the sqlite database:

Now let’s have a deeper look at few elements here. The DDL for the schema is as follows:

CREATE TABLE "test::Person" (object_id INTEGER NOT NULL, name TEXT, age INTEGER, height REAL);

This schema is created internally by the library. I am just showing it here for reference.

The data is as follows:

Persistent Store

oid_high oid_low name age height
1 90023498019372 Brainless_1 11 5.6
2 90023527619226 Brainless_2 12 5.6
3 90023537497149 Brainless_3 13 5.6
4 90023553459526 Brainless_4 14 5.6
5 90023562946990 Brainless_5 15 5.6

As we need the table, the structure and the types are all chosen for you. “oid_high” is the “rowid” default key for sqlite.

Internals

Some of the internals of the ORM are as follows.

Reflection

Bun internally uses simple reflection to generate take care of compile time type information. There is a plan to extend it a little so it can be more useful.

GENERATE_BINDING

This macro will generate all the binding for the objects at compile time. All the template specialization is created using this macro. It should be safe to use the macro in multiple headers or CPP files.

The following should be passed to the macro:

(<Class name, should include the namespace details too>, Members to persist …)

The member list can be partial class members too. Say we have a handle in one of the objects we use, there is no point to store it in the db. In this case, we can omit the handle and persist all the other features. This way, only the given fields will be populated.

PRef

PRef is one of the central elements in the library. It holds the object that needs to be persisted. It also contains the oid of the object, which is independent of the actual object. Few rules to make a object persistent:

  • The member that needs to be persisted has to be public.
  • PRef maintains the ownership of the object and deletes the object when it goes out of scope.
  • If we assign a PRef to another, the PRef the former loses the ownership of the object. Just like a unique_ptr. Actually, PRef stores the object in a unique_ptr underneath.
  • Before persisting objects, we have to create the schema (using blib::bun::createSchema<>()) and generate the bindings (using GENERATE_BINDING( (test::Person, name, age, height) );)
  • It also contains the md5 sum of the object at a particular instance. So if there is no change in the object, then it won’t persist it. I have it as in my own use I keep a timestamp of the update. I do not want to update the object everytime. For this public release, I am omitting the time stamp.

Insert or Update

How does the library know if we want to insert or update the database? This happens with the md5 of the object. If the md5 has some value, then it is an update else it’s an insert. The following query is automatically generated for the insert:

INSERT INTO 'test::Person' (object_id,name,age,height) VALUES(91340162041484,'Brainless_4',14,5.6)

Search

Searching in Bun is quite easy.  There are different mechanisms to search.

  • Oid Search: We can get all the Oids using the method 
// The return type is std::vector<blib::bun<SimpleOID<test::Person>>
const auto person_oids = blib::bun::getAllOids<test::Person>();
  • Search all objects of a type: We can get all the objects in the database as a vector of objects
// std::vector<blib::bun::Pref<test::Person>>
const auto person_objs = blib::bun::getAllObjects<test::Person>();
  • Object EDSL: We can search through the EDSL query that Bun provides. The EDSL is implemented using boost proto library. The query is checked in compile time by the C++ compiler. When GENERATE_BINDING is called it creates some special variables.

          For example: For the Person class the GENERATE_BINDING generates the following 

          bun::query::F<test::Person>::name
          bun::query::F<test::Person>::age
          bun::query::F<test::Person>::heigh

         The bun::query::F class of Bun will be specialized with all the fields of Person class.

         To apply any kind of filters you just need to use the “where” function like:

 // The where(Query) is a lazy function, it does not query the db.

 // The actual execution is done in the object() function

 const auto objs_again = bun::query::From<test::Person>().where( valid_query ).objects();

// We can also join queries or filters using && or the || operator

const auto objs_again = bun::query::From<test::Person>().where( valid_query && valid_query ).objects();

History

  • Alpha 1 (16th May 2016): Initial version of the library
  • Alpha 2 (2nd July 2016): Implementing the Bun EDSL

Next Features

  • Error handling
  • EDSL query language
  • Persisting std::vector members

Help Needed

Anyone who is interested in the development of the library is welcome. Some one with good knowledge of C++11 will be helpful.

All the users/prospective users, please let me know the features that they expect in the libraries.

Help with formalizing the query syntax.

出处:https://www.codeproject.com/Articles/1100449/Cplusplus-Object-Relational-Mapping-ORM-Eating-the

关于作者
ultracpy
评论

你必须 登录 提交评论