2

I need to develop a manager that takes a struct and stores its members to a database row, and that fills a struct with values from a database row again. Objects of the same struct are stored in the same table. This is our example strcut.

struct Person
{
    std::string Name;
    int Age;

    // more members that shouldn't be stored or loaded
    // ...
};

My goal is to load and store structs into their table with one line.

// example for storing
unsigned int id = 42;          // not hardcoded of course
Person paul = { "Paul", 8 };   // instantiate the struct
Data->Save<Person>(id, *paul); // table name is derived from template argument

// example for fetching
unsigned int id = 42;
Person paul;
Data->Load<Person>(id, *paul);

All structs could be forced to implement serialization and deserialization functions through inheritance, or I could make use of macro based reflection as suggested by Matthieu M. in the answers. But I cannot make changes to the structs that get stored. So I decided that those structs must be registered first, providing their serialization and deserialization functions. (By which I mean conversion from an instance of the struct to a database row object and vice versa.)

class Data
{
    typedef std::function<row(T*) Serialization;
    typedef std::function<void(row, T*) Deserialization;
    typedef std::pair<Serialization, Deserialization> Registereds;
    std::unordered_map<std::type_index, Registered> List;
public:
    template <typename T>
    Register<T>(std::function<row(T*) Serialize,
                std::function<void(row, T*) Deserialize)
    {
        // create a table based on the template argument
        std::string name = typeid(T).name();
        // ...

        // add conversion functions to the list
        auto index = std::type_index(typeid(T));
        List.insert(index, std::make_pair(Serialize, Deserialize));
    }

    // load and save functions shown above go here
    // ...
};

You may have already noticed the row data type I used in this example. That exactly is my problem! For every struct the row contains different fields of different types. So what data structure can I use for holding a row?

I though about using some kind of std::tuple. For example if a tuple could have string keys, that would be a possibility. Then, I would have to completely templalize everything to work with different structs. It could look similar to this draft below. But I have to idea how to store the conversion functions of different structs in the same List then, without using void* pointers.

Data->Register<Person, std::string, int>(
    [](Person *Instance) -> std::tuple<std::string, int>{ /* ... */ }, 
    [](std::tuple<std::string, int> Row, Person *Instance) -> void{ /* ... */ }
);

How can I represent the database row object? If my template approach is a good way, how can I templatize everything?

Update in response to Matthieu M.'s answer

You suggested using type traits. This is what I tried. But I still don't know how to define the type for representing a row.

Traits

template <class T>
struct data_traits{ };

struct data_traits<Person>
{
    struct row{ int Age; std::string Name; }; // right to do here?
    row serialize(Person *Instance)
    {
        // ...
    }
    void deserialize(row Data, Person *Instance)
    {
        // ...
    }
};

Data manager

class Data
{
    template <typename T>
    void Load(uint64_t Id, T *Instance)
    {
        auto traits = data_traits<T>;

        std::string query = "SELECT * FROM "
                          + typeid(T).name()
                          + " WHERE id = "
                          + std::to_string(Id);
        // fetch result from database
        // ...

        traits::row result;
        // convert SQL result into type dependent row type
        // but how to do that?
        // ...

        traits.deserialize(result, Instance);
    }
};
danijar
  • 27,743
  • 34
  • 143
  • 257
  • 3
    Can you use Boost? `std::unordered_map` seems reasonable. –  Aug 18 '13 at 09:57
  • Through some recursion you could probably find the string (assuming `std::string`) key underlying in the `std::tuple`. You could probably use indices but the issue becomes that you're doing a lot of comparisons, which may or may not be an actual bottleneck. – Rapptz Aug 18 '13 at 10:00
  • 3
    The real question is, how do you plan to use it ? The idea of using a runtime key is somewhat strange in that in C++ you need to know the static type of the value associated to the key. It seems to me that you are suffering from the X/Y problem; your original problem is X, you created a solution that hinges on getting Y done and thus are asking for how to solve Y... but maybe we could give you a better answer if you exposed what the original problem is: context, context, context! – Matthieu M. Aug 18 '13 at 10:03
  • @MatthieuM. The structure I'm looking for is for holding on row of a SQL database. So it maps values of given types to given column names. Is this what you asked for? – danijar Aug 18 '13 at 12:08
  • @danijar do you determine the db metadata (columns + types) at runtime or compile time? And if it is known at compile time, which format do you have? – KillianDS Aug 18 '13 at 12:28
  • @KillianDS I want to determine that at run time, but if this isn't possible, I would have to go with compile time. Please see the example code I added to the question. – danijar Aug 18 '13 at 12:44
  • `std::tuple` instantiations cannot be created at runtime either. – rodrigo Aug 18 '13 at 13:25
  • @rodrigo Okay, so I'll have to go with templates. I rewrote the question to explain the context better. – danijar Aug 18 '13 at 13:50
  • @danijar: I finally get what you want => basically, a simplistic ORM. In standard C++ this is unfortunately infeasible because it requires a limited form of **reflexion**. There are (some) work-arounds or you can accept not to use standard c++. – Matthieu M. Aug 18 '13 at 14:53

1 Answers1

1

Note: I deleted my previous answer since it was on an entirely different interpretation of the question; I still leave it (deleted) as reference if anybody wishes to see it.

What you are asking for could be done in a language having reflection. Full blown reflection fortunately is not required, still C++ does not offer any out of the box.

Warning: I personally consider the need dubious. Asking for a 1-to-1 mapping between a structure (and its field names) and the database table (and its column names) may cost more in terms of maintenance than it is worth. The notable question is how to retain backward/forward compatibility when changing the database schema. /Warning


A first standard way to add in reflection is to consider BOOST_FUSION_ADAPT_STRUCT.

struct Person {
    struct Name: Field<std::string> { static char const* N() { return "name"; } }
    struct Age: Field<int> { static char const* N() { return "age"; } }

    Name name;
    Age age;
}; // struct Person

BOOST_FUSION_ADAPT_STRUCT(
    Person,
    (Name, name)
    (Age, age))

It's a double-trick:

  • A Fusion sequence can be iterated over at runtime
  • Each attribute is augmented by a name

Note: the same thing can be achieved by using a Boost.Fusion.Map sequence.

Of course, this is not perfect since it actually requires you to alter the definition of the structure. It is probably possible, though might be tough, to come up with an equivalent macro that does the name-augmenting in place to avoid having to alter the base structure and if you managed that it would be worth posting your own solution :)


Another solution is to actually go for a fuller reflection based approach. A solution to obtain reflection based on C99 variadic macros was added some type ago by @Paul here.

If you are into macros, I would note it could probably be adapted via Boost.Preprocessor to work with its sequences (would add a couple more parentheses but be standard).

Once again, though, this require modifying the declaration of the structure.


Personally, I would love to see the first solution enhanced to adapt 3rd party structures. On the other hand, given the strong coupling introduced between your Object Model and the Database Schema it might be best to restrain yourself to your own types already.

Community
  • 1
  • 1
Matthieu M.
  • 251,718
  • 39
  • 369
  • 642
  • I didn't know of those ways to add reflection to C++. So I need to get familiar with the Boost concept first, before developing my own solution. Anyhow it would be cool to just mark members by a macro to automatically assign a database column to them. Even though that it's clearly not necessary in my case, I may try that for learning purpose. – danijar Aug 18 '13 at 15:26
  • Since I started implementing, my initial problem, the need of a data structure to hold a row, came up again. I updated the question to explain this issue as brief and concise as possible. Could you please share your thoughts about this? – danijar Sep 02 '13 at 08:31
  • @danijar: What you are doing looks very much like *traits*. For example, in the standard library the second template parameter of `std::basic_string` is by default `std::char_traits`. By creating a "base" traits and specializing it for each structure you need to store, the trait can provide (de)serialization methods and you can access them by template code (ie, no registration necessary). And of course, the beauty of it is that you need not actually modify `Person` itself. – Matthieu M. Sep 02 '13 at 09:33
  • Thanks for pointing that out. I will check that out. The trait plays the role of registration then, so I don't know what is better. But traits seem to be more popular than my conversion function approach, thus more readable for other people. Would you still store the traits in the `List` object from my code example? The type of the list could be `std::unordered_map` then. – danijar Sep 02 '13 at 10:14
  • @danijar: no, not at all. That is the advantage of the trait. When you write `DbTrait::Serialize` the compiler knows which specialization of `DbTrait` to use without an additional registry. And because there is no additional registry, there is no need for base classes and uniform function signatures. – Matthieu M. Sep 02 '13 at 11:20
  • Alright, I read into type traits and I think I understand it. But would I define the conversion functions in the traits? Maybe I could even define the row data type there. I appended my code draft to the question. Moreover, is there a standard where the trait is provided, with the type or with were it's used? By the way, I wouldn't need a base class for registration since std::function would allow me to store conversion functions, so that they can be provided externally. – danijar Sep 05 '13 at 16:22