Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for associations #11

Open
Oipo opened this issue Nov 2, 2014 · 15 comments
Open

Support for associations #11

Oipo opened this issue Nov 2, 2014 · 15 comments

Comments

@Oipo
Copy link

Oipo commented Nov 2, 2014

It would be really nice to be able to do

friend class hiberlite::access;
template<class Archive>
void hibernate(Archive & ar)
{
    ar & HIBERLITE_NVP(_Name);
    ar & HIBERLITE_ONETOMANY(_Employers);
}

...

vector<Employer> _Employers;

@paulftw
Copy link
Owner

paulftw commented Nov 3, 2014

You can serialize a vector in Hiberlite, so

     ar & HIBERLITE_NVP(_Employers)

would create another table and store employers there. Other C++ way of doing it is:

    vector< bean_ptr<Employer> > employers;

If those a stored and referenced from elsewhere.
Would this adequately address the need for ONETOMANY?

I'm not saying that associations is a bad idea, and more than happy to discuss this feature.
Most other ORM libraries let you do something like that, and we should definitely support this use case.

However, I have some concerns on the philosophical level:

The term "association" comes from the database design world, it's not something present in STL, or in
usual C++ object hierarchies. So I'm worried that it may not be in the spirit of Hiberlite and there could be a "more object-oriented" way expressing that

I'm sure you don't ask to support association for the sake of it, you want achieve certain behaviour of your code. There may be an easier and "more object-orientier" :) way of achieving that behaviour.

@Oipo
Copy link
Author

Oipo commented Nov 4, 2014

I agree wholeheartedly that there's a nice object oriented way of dealing with my issue, rather than simply copying database terms.

Let me demonstrate what happens.

class Person:

class Person
{
private:
    friend class hiberlite::access;
    template<class Archive>
    void hibernate(Archive & ar)
    {
        ar & HIBERLITE_NVP(_Name);
        ar & HIBERLITE_NVP(_Employer);
    }
public:
    Person() = default;
    string _Name;
    Employer _Employer;
}

Class Employer:

class Employer
{
private:
    friend class hiberlite::access;
    template<class Archive>
    void hibernate(Archive & ar)
    {
        ar & HIBERLITE_NVP(_Name);
    }
public:
    Employer() = default;
    string _Name;
}

main.cpp:

void main()
{
    db = new hiberlite::Database("sample.db");
    db->registerBeanClass<Person>();
    db->registerBeanClass<Employer>();

    db->dropModel();
    db->createModel();
}

This creates the following underlying table structures:
Person table:

_Name (text)
hiberlite_id (integer)

Employer table:

_Name (text)
hiberlite_id (integer)

Person__Employers_person table:

hiberlite_entry_indx (integer)
hiberlite_id (integer)
hiberlite_parent_id (integer)
person__Name (text)

This is not the table structure that I expect. I expect the following:
Person table:

_Name (text)
_EmployerId (Integer)
hiberlite_id (integer)
FOREIGN KEY(_EmployerId) REFERENCES Employer(hiberlite_id)

Employer table:

_Name (text)
hiberlite_id (integer)

Now, what I'd also like to do, is map persons to an employer, with exactly the same table structure as I expect above. This is going to be especially tricky due to circular dependency, and since it's rather late, I have no idea how to best solve this. To show my intent, here's an example of magic thinking:

class Employer
{
private:
    friend class hiberlite::access;
    template<class Archive>
    void hibernate(Archive & ar)
    {
        ar & HIBERLITE_NVP(_Name);
        ar & HIBERLITE_NVP_INVERSE(_Persons); // --- EDIT HERE ---
    }
public:
    Employer() = default;
    string _Name;
    vector<Person> _Persons; // --- EDIT HERE ---
}

@d-led
Copy link
Collaborator

d-led commented Nov 4, 2014

hm, maybe using the SRP and creating another object for that should be a cleaner way to solve the problem? Do you need the back-reference persistent? If not, you might just create a map once at application start and keep it up to date. Correct me, please, if I haven't understood the problem :)

@Oipo
Copy link
Author

Oipo commented Nov 4, 2014

Well, there are two problems here:

  1. The current way of generating the table structure seems to be off IMO
  2. No support for back-reference.

As for the back-reference, I don't know what you mean with persistence?

On 4 November 2014 07:11, Dmitry Ledentsov [email protected] wrote:

hm, maybe using the SRP and creating another object for that should be a
cleaner way to solve the problem? Do you need the back-reference
persistent? If not, you might just create a map once at application start
and keep it up to date. Correct me, please, if I haven't understood the
problem :)


Reply to this email directly or view it on GitHub
#11 (comment).

@paulftw
Copy link
Owner

paulftw commented Nov 4, 2014

SRP sounds like an overkill to me. We do not support foreign keys, and that may be part of the problem here.

How would you write it in C++? I would come up with something like:

class Person {
    Employer* employer; // note this is a pointer, because storing employer as value makes no sense.
    string _Name;
}

class Employer {
    vector<Person> persons; // in some cases you'd want vector of pointers,
                                             // if persons "live" elsewhere, not in employer.
    string _Name;
}

Then [what seems to me as] a natural translation to hiberlite API is:

class Person {
    ... hibernate code ...
    bean_ptr<Employer> employer; // because pointer
    string _Name;
}

class Employer {
    ... hibernate code ...
    vector<Person> persons;
    string _Name;
}

Another good question to ask is what would Boost Serialization? I think quite similar to the above.
That was my argument on why we don't need one-to-many and many-to-one.

But there are a few problems with the C++ way:

  • you have to update two objects when a person changes employer
  • inserting/removing elements in a vector is expensive and "artificial" - I only suggested vector because it's what C++ has to offer, not because it's the best possible tool for the job.

SQL would be more convenient & powerful here. It also seems to me that SQL association is closer to the "natural language" of what @Oipo wants to implement.

I am now leaning towards supporting associations - it may actually be a good idea.
Any thoughts?

@d-led
Copy link
Collaborator

d-led commented Nov 4, 2014

P.S. with SRP I meant the Single Responsibility Principle, sorry for the ambiguity :)

@d-led
Copy link
Collaborator

d-led commented Nov 4, 2014

@Oipo:

No support for back-reference.
As for the back-reference, I don't know what you mean with persistence?

What I mean by persistence is the presence of that back-reference in the database. If you can fit it in memory and it doesn't cost you much, one can do the simplest thing first. There's always a possibility of adding features, the question is, why C++ and not C#+EF then?

Now, what I'd also like to do, is map persons to an employer, with exactly the same table structure as I expect above. This is going to be especially tricky due to circular dependency, and since it's rather late, I have no idea how to best solve this.

If you just want to solve it in your case, go for the examples, @paulftw has sketched. Build the C++-side back-reference, i.e. after initializing your data structures.

@paulftw:

I am now leaning towards supporting associations - it may actually be a good idea. Any thoughts?

I can't say much on the subject, since I only occasionally need SQL. If I see a convincing example, I might be able to give a judgement. If the value is learning, then that feature might be fun to implement.

But there are a few problems with the C++ way: you have to update two objects when a person changes employer

While EF6 is striving to take SQL hell out of C# coders routine, the coders themselves run into troubles in grey zones. i.e. while not exactly the same problem, but ... SO:EF6 code-first inheritance back-reference

I am sorry for the meta-talk, but I like hiberlite for its cleanliness. Circular references create chaos in software design, whatever the software is. Thus, I'd recommend two rants from Uncle Bob.

As for the software outside the library, I'd suggest to abstract the persistence (DB) layer further at the application level, and write a simple implementation of back-references in pure C++, test it, and then try to implement that in hiberlite, if the use-case is convincing.

@Oipo:
Associations could perhaps indeed be interesting, but for the concrete example, I find the concrete OO design wrong here. A Person does not 'have' an Employer, thus an Employer doesn't need to have a back-reference to Persons. At most, an Employer has a list of Persons or better a list of their IDs, be it database keys or not. Since source code is a liability, it's never too late to throw it away, but keep the true assets: requirements and specification.

@Oipo
Copy link
Author

Oipo commented Nov 4, 2014

Thanks for your clarification, the SRP idea could probably work in my case of back-reference. However, I do wish to clarify that my Person <-> Employer example is just an example to show a situation where I find odd things. Combined with the lack of internal documentation and also the lack of explanation of why certain design decisions were made, I find myself encountering things I would like to see but aren't yet present in Hiberlite.

The use case I have is to do efficient SQL queries, using SQLite's already diminished capabilities. SQLite's capabilities are chosen exactly because they are often used. One of those often used capabilities is to fetch a collection based on a foreign key. As such, I think paul correctly identified one of the problems, namely the missing support for foreign keys in hiberlite. Of course, fetching collections based on any columns is another, separate, functionality that I would like to have, especially in conjunction with joins.

An example (in pure SQL) to select all Persons working at an Employer whose name contains 'Eric':

select p.*, e.* from Person p
inner join Employer e on p._EmployerId = e.hiberlite_id
where e.Name LIKE '%Eric%'

Ideally, doing this in hiberlite would cause each Person to have an initialized Employer and each Employer to have a filled list of Persons. All in one query, such as in nHibernate(which I believe is more or less similar to EF, but as I lack experience in EF, I can't say for sure).

However, what I currently have to do, regardless of using SRP or not, in hiberlite is the following:

auto employersList = db->getAllBeans<Employer>();
auto employer  = find_if(employersList.begin(), employersList.end(), [](hiberlite::bean_ptr<Employer> item){return item->_Name.find("Eric") != std::string::npos; });

auto personsList = db->getAllBeans<Person>();
decltype(entityList) correctPersonsList(personsList.size());
auto it = copy_if(personsList.begin(), personsList.end(), correctPersonsList.begin(),
        [&employer](hiberlite::bean_ptr<Person> item)
        {return item->_EmployerId == (*employer).getId()});
correctPersonsList.resize(distance(personsList.begin(), it));

Of course, I may be missing crucial functionality that is already in hiberlite here. But if I understand correctly, this is needlessly complex, creates two queries with disastrous performance by pulling in ALL instances of both Person and Employer and destroys the one thing SQL was made for: joining arbitrary collections.

@d-led
Copy link
Collaborator

d-led commented Nov 6, 2014

Thank you for your answer too, @Oipo. If I understand you correctly now, you'd like to see an abstraction for queries, such as joins in hiberlite.

@Oipo
Copy link
Author

Oipo commented Nov 7, 2014

Sorry for the confusion, yes, such an abstraction is what I'd like. I thought, however, that associations were necessary to implement those. Do you have other ideas on how to achieve query abstractions? And should I make a new issue with a better title?

@d-led
Copy link
Collaborator

d-led commented Nov 8, 2014

I haven't looked at that one: quince, but it may suit your needs. The scope is somewhat different, however.

@Oipo
Copy link
Author

Oipo commented Nov 8, 2014

Ah thanks, that library looks promising.

@d-led
Copy link
Collaborator

d-led commented Nov 9, 2014

@paulftw. I'm now also convinced of associations. Having seen quince, one can define a clear difference of scope of the two libraries. I can imagine a fully hidden queries behind an API that would be understandable without SQL jargon, would however be useful for hiberlite users. I can't think of a good replacement name for a join at the moment, but the first step could be abstracting that query

@paulftw
Copy link
Owner

paulftw commented Nov 10, 2014

@d-led quince is nice, though some of its tricks are a mystery for me:

struct point {
    float x;
    float y;
};
QUINCE_MAP_CLASS(point, (x)(y))

Maybe down the road we could look into making quince an underlying SQL engine - would let us remove some code and support other RDBMS.

With associations - how should it be expressed in code?
It's touching both Person and Employer, so where should the declaration live?
There are 4 options, obviously: in P, in E, in P&E, external to both P&E.

I'm not concerned about using SQL jargon - it'd be familiar to most developers, unlike for example "transclusion" in AngularJS. But I agree that "join" is far from perfect.

Let's decide what we want the code to look like, and then find right words to fill in the gaps.

@d-led
Copy link
Collaborator

d-led commented Nov 12, 2014

@paulftw I'll think about it ... might take time and reading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants