Skip to content
Yevgeniy Zakharov edited this page Sep 1, 2021 · 1 revision
1. Storage is created with `auto` type in all the examples. How can I get its' type to keep a storage as a variable?

There are two ways of doing it: quick & binary fat and slow & size effective

Quick & binary fat

It is easy. First of all you should move making a storage into a separate inline function with auto return type. Next make alias of the result of this function execution.

struct Employee {
    int id;
    std::string name;
    int age;
    std::string address;
    double salary;
};

inline auto initStorage(const std::string &path) {
    using namespace sqlite_orm;
    return make_storage(path,
                        make_table("COMPANY",
                                   make_column("ID",
                                               &Employee::id,
                                               primary_key()),
                                   make_column("NAME",
                                               &Employee::name),
                                   make_column("AGE",
                                               &Employee::age),
                                   make_column("ADDRESS",
                                               &Employee::address),
                                   make_column("SALARY",
                                               &Employee::salary)));
}

using Storage = decltype(initStorage(""));

Storage storage = initStorage("storage.sqlite");

Now Storage is a type of your custom storage with your schema. Note that if you change schema Storage also will have different type. I mean there is no common storage_t type cause storage_t is templated just like std::vector: std::vector<int>std::vector<char>. Please consider inline in initStorage declaration. It is important cause otherwise compilation may fail with duplicate symbols error. Also please check update example here https://github.com/fnc12/sqlite_orm/blob/master/examples/update.cpp for more detailed code.

Slow & size effective

First way may be not efficient cause inline functions enlarge your binary size. You probably will never face such kind of issue if you never make an inline function with making a storage with large amount of tables and columns. But once you do so and binary size is important you'd want to fix it. One of main advantage (and disadvantage simultaneously) is that sqlite_orm and storage_t especially are made with templates. Templates help using such a brilliant API with no runtime overhead. But templates sometimes are not comfortable to write user code. Consider storage_t as a C++ lambda class. Every C++ lambda instance has an unique class if it has unique capture list or/and arguments list. 'But I can store lambda in std::function easily' you'd say. This is right. But once you change arguments list in your lambda you also must change template arguments for your std::function:

std::function<void(int, std::string)> myFunc = [&](int arg0, std::string arg1){
    //..
};
std::function<void(int)> myFunc2 = [&](int arg){
    //..
};

We changed arguments list from int, std::string to int in lambda body and we also must change it in myFunc2 variable declaration. Same thing with storage_t: once you add a new table/column/index during development you also have to change declaration type if you gave up using auto. Example:

    using namespace sqlite_orm;
    struct Person {
        int id = 0;
        std::string firstName;
        std::string lastName;
    };
    auto autoPersonStorage = make_storage("",
                                          make_table("persons",
                                                     make_column("id", &Person::id, primary_key()),
                                                     make_column("first_name", &Person::firstName),
                                                     make_column("last_name", &Person::lastName)));
    
    template<class O, class T, class ...Op>
    using Column = internal::column_t<O, T, const T& (O::*)() const, void (O::*)(T), Op...>;

    using Storage = internal::storage_t<internal::table_t<Person,
    Column<Person, decltype(Person::id), constraints::primary_key_t<>>,
    Column<Person, decltype(Person::firstName)>,
    Column<Person, decltype(Person::lastName)>>>;  // <== this is a very 'short' case
    
    static_assert(std::is_same<Storage, decltype(autoPersonStorage)>::value, "");
    
    Storage explicitPersonStorage = std::move(autoPersonStorage);

And you can see line starting with using Storage has an explicit storage type declaration. It is a pure honest way to declare a storage type without inline functions and auto deducing just like we used to make it before C++11 released.

Main disadvantage of this way may be faced once you need to add/modify/remove table(s). Assume we need to add one more field (companyName) to Person class and to map it with SQLite storage:

    struct Person {
        int id = 0;
        std::string firstName;
        std::string lastName;
        std::string companyName;
    };
    
    auto autoPersonStorage = make_storage("",
                                          make_table("persons",
                                                     make_column("id", &Person::id, primary_key()),
                                                     make_column("first_name", &Person::firstName),
                                                     make_column("last_name", &Person::lastName),
                                                     make_column("company_name", &Person::companyName)));   // one more line here
    template<class O, class T, class ...Op>
    using Column = internal::column_t<O, T, const T& (O::*)() const, void (O::*)(T), Op...>;

    using Storage = internal::storage_t<internal::table_t<Person,
    Column<Person, decltype(Person::id), constraints::primary_key_t<>>,
    Column<Person, decltype(Person::firstName)>,
    Column<Person, decltype(Person::lastName)>,
    Column<Person, decltype(Person::companyName)>>>;  //  .. and one more line here
    
    static_assert(std::is_same<Storage, decltype(autoPersonStorage)>::value, "");
    
    Storage explicitPersonStorage = std::move(autoPersonStorage);

If we need to add a new table we need to write even more lines:

    struct Person {
        int id = 0;
        std::string firstName;
        std::string lastName;
        std::string companyName;
    };
    struct Responsible {
        int id = 0;
        std::string person_fid;
        std::string percentage;
    };
    
    auto autoPersonStorage = make_storage("",
                                          make_table("persons",
                                                     make_column("id", &Person::id, primary_key()),
                                                     make_column("first_name", &Person::firstName),
                                                     make_column("last_name", &Person::lastName),
                                                     make_column("company_name", &Person::companyName)),
                                          make_table("responsibles",
                                                     make_column("id", &Responsible::id, primary_key()),
                                                     make_column("person", &Responsible::person_fid),
                                                     make_column("percentage", &Responsible::percentage)));
    
    template<class O, class T, class ...Op>
    using Column = internal::column_t<O, T, const T& (O::*)() const, void (O::*)(T), Op...>;

    using Storage = internal::storage_t<
    internal::table_t<Person,
    Column<Person, decltype(Person::id), constraints::primary_key_t<>>,
    Column<Person, decltype(Person::firstName)>,
    Column<Person, decltype(Person::lastName)>,
    Column<Person, decltype(Person::companyName)>>,
    internal::table_t<Responsible,
    Column<Responsible, decltype(Responsible::id), constraints::primary_key_t<>>,
    Column<Responsible, decltype(Responsible::person_fid)>,
    Column<Responsible, decltype(Responsible::percentage)>>>;
    
    static_assert(std::is_same<Storage, decltype(autoPersonStorage)>::value, "");
    
    Storage explicitPersonStorage = std::move(autoPersonStorage);

So type is too long. Is there a way to make it better? Yes. One make a class like std::any - any_storage which will have get_all, get, update, select etc functions with dynamic storage binding. Disadvantage of it - if you call myAnyStorage.get<MyClass>(objectId); if MyClass is not mapped we will be unable to get compile time error - only runtime. By advantage is that AnyStorage will not be template class. Note: classes and functions from sqlite_orm::internal namespace are not public so they compatibility may be broken in any version. Please be careful.

2. I'd like to access `sqlite3*` handle of my database. How can I do it?

Class storage_t has on_open of type std::function<void(sqlite3*)> member which can be assigned with lambda function like this:

auto storage = make_storage(...);
storage.on_open = [](sqlite3 *db) {
    // do whatever you want except database closing
};

on_open called every time storage opens a database. If you call storage.open_forever() then on_open will be called at once and only once. Initially on_open is designed for encrypted databases not for database connection lifetime management. So if you call sqlite3_close(db); inside on_open callback then it will lead to two calls of sqlite3_close with the same database handle cause storage must manage connection lifetime not client. But if you call something like SELECT right inside on_open it will fail nothing despite this API was not designed for it.

If you have any question feel free to post an issue https://github.com/fnc12/sqlite_orm/issues[here]