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

Back-referencing relationship: am I doing it a wrong way? #472

Closed
vrurg opened this issue Mar 9, 2021 · 14 comments
Closed

Back-referencing relationship: am I doing it a wrong way? #472

vrurg opened this issue Mar 9, 2021 · 14 comments

Comments

@vrurg
Copy link
Contributor

vrurg commented Mar 9, 2021

I'm trying this simple structure:

model m1 is nullable {
    has UInt $.id is serial;
    has Str $.name is column;

    has $.m2 is relationship(*.m1-id, :model<m2>);
}

model m2 is nullable {
    has UInt $.m1-id is referencing(*.id, :model<m1>);
    has Str $.descr is column;
}

say schema(m1, m2).create;

say (m1.^create: :name<test>);

It ends up with:

Red::Schema.new(models => {:m1(m1), :m2(m2)})
No such method 'm1-id' for invocant of type 'm1'
  in method rel at /home/vrurg/src/Raku/rakudo/install/share/perl6/site/sources/9740DDE0E85E093DCDF40F690C620BD9BAEE9078 (Red::Attr::Relationship) line 38
  in method set-data at /home/vrurg/src/Raku/rakudo/install/share/perl6/site/sources/9740DDE0E85E093DCDF40F690C620BD9BAEE9078 (Red::Attr::Relationship) line 56
  in block  at /home/vrurg/src/Raku/rakudo/install/share/perl6/site/sources/46A4802DDE77493E7983803A0896FA211823CF80 (MetamodelX::Red::Dirtable) line 172
  in method <anon> at /home/vrurg/src/Raku/rakudo/install/share/perl6/site/sources/46A4802DDE77493E7983803A0896FA211823CF80 (MetamodelX::Red::Dirtable) line 160
  in method create at /home/vrurg/src/Raku/rakudo/install/share/perl6/site/sources/A963FC4C1C5D6923CAA0DC1451EE51EA254DA1ED (MetamodelX::Red::Model) line 483
  in block <unit> at t/db/010-account.rakutest line 36

Basically, the idea is that m2 is a kind of profile attached to a user account m1. It's id will always be the same as account's id. And there will always either be a profile record, or not. So, $m1.m2 could be either defined or undefined.

Ok, if Red expected a record in m2 to always exists for corresponding m1 record then I shall create one. So, I try:

m1.^create: :name<test>, :m2{:descr<descr>};

But it results in the same error as above.

@FCO
Copy link
Owner

FCO commented Mar 9, 2021

Red's relationships, by default, expects a relationship $ to @, so if it's a @, and it will searches the id on the $ one. If on that relationship it's $ to $ (to-one relationship), it will try to find the id on it self (on both sides). To change that, you can use the experimental feature "has-one", that adds the parameter :has-one to the is relationship trait to mean it's a 1-to-1 relationship.
For example:

use Red <has-one>;

model m1 is nullable {
    has UInt $.id is serial;
    has Str $.name is column;

    has $.m2 is relationship(*.m1-id, :model<m2>, :has-one);
}

model m2 is nullable {
    has UInt $.m1-id is referencing(*.id, :model<m1>);
    has Str $.descr is column;
}
red-defaults "SQLite"; my $*RED-DEBUG = True;
say schema(m1, m2).create;

say (m1.^create: :name<test>);

that prints:

SQL : BEGIN
BIND: []
SQL : CREATE TABLE m1(
   id integer NOT NULL primary key AUTOINCREMENT,
   name varchar(255) NULL 
)
BIND: []
SQL : CREATE TABLE m2(
   m1_id integer NULL references m1(id),
   descr varchar(255) NULL 
)
BIND: []
SQL : COMMIT
BIND: []
Red::Schema.new(models => {:m1(m1), :m2(m2)})
SQL : INSERT INTO m1(
   name
)
VALUES(
   ?
)
BIND: ["test"]
SQL : SELECT
   m1.id , m1.name 
FROM
   m1
WHERE
   _rowid_ = last_insert_rowid()
LIMIT 1
BIND: []
SQL : SELECT
   m1.id , m1.name 
FROM
   m1
WHERE
   m1.id = 1
LIMIT 1
BIND: []
m1.new(id => 1, name => "test")

@FCO
Copy link
Owner

FCO commented Mar 9, 2021

I'd like to have more people testing it to let it evolve and make it not experimental anymore.

@vrurg
Copy link
Contributor Author

vrurg commented Mar 9, 2021

Thanks! I thought that a scalar relationship is not experimental anymore.

In the form you provided it does create the schema. But then troubles begin. m1 itself is created, no problem. Then I try:

m1.^create: :name<test>, :m2{ :descr<test-descr> };

It results in

No such method 'm1-id' for invocant of type 'm1'
  in method rel at /home/vrurg/src/Raku/rakudo/install/share/perl6/site/sources/9740DDE0E85E093DCDF40F690C620BD9BAEE9078 (Red::Attr::Relationship) line 38

I try it other way around:

my $m1 = m1.^create: :name<test>;
$m1.m2.create: :descr<test-descr>;
say ($m1 = m1.^find: :name<test>);
say m2.^find: :descr<test-descr>;

And it goes without an error, but:

m1.new(id => 1, name => "test")
Nil

So, we end up with no m2.

Eventually, the only one which works is:

my $m1 = m1.^create: :name<test>;
m2.^create: :m1-id($m1.id), :descr<test-descr>;

Not the most user-friendly one, though. For the sake of DWIM, it'd be great if the other two work as well. :)

@FCO
Copy link
Owner

FCO commented Mar 9, 2021

Yes... that's a bug indeed. I'll try to fix that as soon as possible.

@FCO
Copy link
Owner

FCO commented Mar 9, 2021

I think I've "kinda fixed it"... but there is a problem...

 fernando@MBP-de-Fernando  ~/Red   master ●  
raku -I. -e '


use Red <has-one>;

model m1 is nullable {
    has UInt $.id is serial;
    has Str $.name is column;

    has $.m2 is relationship(*.m1-id, :model<m2>, :has-one);
}

model m2 is nullable {
    has UInt $.m1-id is referencing(*.id, :model<m1>);
    has Str $.descr is column;
}
red-defaults "SQLite";                       
say schema(m1, m2).create;

say (m1.^create: :name<test>, :m2{ :descr<bla> });
.say for m1.^all;
.say for m2.^all;
'
Red::Schema.new(models => {:m1(m1), :m2(m2)})
m1.new(id => 1, name => "test")
m1.new(id => 1, name => "test")
m2.new(m1-id => Any, descr => "bla")

I don't know how to populate m1-id once the m1 object wasn't created when I'm creating the m2 one.
Maybe I should create it on the opposite order... I'll give it a try.
Any suggestions?

FCO added a commit that referenced this issue Mar 9, 2021
@FCO
Copy link
Owner

FCO commented Mar 9, 2021

@vrurg now it seems to be working! It will work for a single id (I have to find a way to make it work for multiple ids)

raku -I. -e '


use Red <has-one>;

model m1 is nullable {
    has UInt $.id is serial;
    has Str $.name is column;

    has $.m2 is relationship(*.m1-id, :model<m2>, :has-one);
}

model m2 is nullable {
    has UInt $.m1-id is referencing(*.id, :model<m1>);
    has Str $.descr is column;
}
red-defaults "SQLite";
say schema(m1, m2).create;

say (m1.^create: :name<test>, :m2{ :descr<bla> });
.say for m1.^all;
.say for m2.^all;
'
Red::Schema.new(models => {:m1(m1), :m2(m2)})
m1.new(id => 1, name => "test")
m1.new(id => 1, name => "test")
m2.new(m1-id => 1, descr => "bla")

Could you test it out, please?

FCO added a commit that referenced this issue Mar 9, 2021
@vrurg
Copy link
Contributor Author

vrurg commented Mar 10, 2021

Ok, so far m1.^create: :name<test>, :m2{ :descr<test-descr> }; does work. But for already existing record $m1.m2.create: :descr<...> – doesn't. I don't consider it a big issue as it is easily replaceable with m2.^create: :m1-id($m1.id), :descr<...>. Less pretty, but functional.

One way or another, it works and I have ways to proceed. Thank you!

Just one question out of curiosity: do we really need :has-one modifier? Red actually knows that the attribute is a scalar and can make assumptions based on that. Can't it?

@vrurg
Copy link
Contributor Author

vrurg commented Mar 10, 2021

Perhaps a useful observation. Here is debug print I get with my almost real life models:

GRP: {is-group => True, name => admins, profile => {description => test description}}
SQL : INSERT INTO accounts(
   name,
   is_group
)
VALUES(
   ?,
   ?
)
BIND: ["admins", Bool::True]
SQL : SELECT
   accounts.id , accounts.name , accounts.password , accounts.is_group as "is-group"
FROM
   accounts
WHERE
   _rowid_ = last_insert_rowid()
LIMIT 1
BIND: []
SQL : SELECT
   accounts.id , accounts.name , accounts.password , accounts.is_group as "is-group"
FROM
   accounts
WHERE
   accounts.id = 1
LIMIT 1
BIND: []
SQL : INSERT INTO account_profile(
   account_id,
   description
)
VALUES(
   ?,
   ?
)
BIND: [1, "test description"]

It feels to me that one SELECT here is redundant. Or, perhaps, it's not but then one of them must not fetch all columns.

Otherwise, when trying to introspect the data I put into a DB here is what I get when refer to a has-one attribute:

No such method 'attr' for invocant of type 'Slip'.  Did you mean any of
these: 'atan', 'Str'?
  in block  at /home/vrurg/src/Raku/BOTradingWeb/../Red/lib/Red/Attr/Relationship.pm6 (Red::Attr::Relationship) line 76
  in code  at /home/vrurg/src/Raku/BOTradingWeb/../Red/lib/Red/Attr/Relationship.pm6 (Red::Attr::Relationship) line 72
  in method <anon> at /home/vrurg/src/Raku/BOTradingWeb/../Red/lib/Red/Attr/Relationship.pm6 (Red::Attr::Relationship) line 71
  in block <unit> at t/db/010-account.rakutest line 57

Haven't golfed it down to something comprehensible and it's actually time to call it a day. So, leaving it here just as a side note. Will try to diagnose tomorrow.

@FCO
Copy link
Owner

FCO commented Mar 10, 2021

Yes, $m1.m2.create do not work. That works for to-N relationships because it returns a ResultSeq, and that's not the case for to-1 or has-1 relationships. Those return the object or Empty. So, it does not have a create method. I've been wondering if it shouldn't return a ResultSeq if there is no object to be returned. That way .create would work.

The reason we need :has-one is: a relationship has 2 ends and by default Red expects one to be $ and the other @. When accessing the $ end, it gets the foreign key column from the same model it is. When accessing the @ one it gets the foreign key from the referenced model. So, if I have a model M1 that has a $.m2 relationship and a M2 with a @.m1s relationship, when accessing M1.m2, it will get the foreign key from M1 (M1.m2-id for example) and doing M2.m1s it will use the fk from M1 (also M1.m2-id). The problem with the has-one relationships is that both sides has $ relationship. So being a has-one relationship, M1.m2 would use M1 to get the fk and M2.m1 would use M2 (it's a $) what's wrong, there is no fk on M2 (we can see that problem we have when not define :has-one on a $-$ relationship). :has-one changes Red's behaviour to even being a $ relationship, search its fk on the referenced model.

@jonathanstowe
Copy link
Contributor

Those return the object or Empty. So, it does not have a create method.

If rather than Empty it returned the type object of the related model it could have a ^create however, the slightly trickier part would be populating the primary key of the created row back into the first row, handwaving slightly , the first object could apply a temporary role to the target model with a closure to populate the foreign key or something I guess.

@FCO
Copy link
Owner

FCO commented Mar 10, 2021

ResultSeq already has a way of doing that, that's how .create on @ relationships work. (https://github.com/FCO/Red/blob/master/lib/Red/ResultSeq.pm6#L356)

That's why I've being thinking on return a ResultSeq when there is no object set.

@vrurg
Copy link
Contributor Author

vrurg commented Mar 10, 2021

With regard to has-one – it's too much into internals of Red for me to get into now. With regard to create, here is an idea to consider:

multi trait_mod:<is> (Attribute:D $a is raw, :$foo!) {
    $a.auto_viv_container = class { 
        method create(*%c) {
            say "CREATING WITH ", %c;
        }
    };
}

class Foo {
    has $.a is foo;
}

my $foo = Foo.new;
$foo.a.create(:1a, :2b);

Apparently, this will require more work to be done correctly. For example, with has $.a is rw is foo Attribute.container_descriptor.default has to be properly set to handle $foo.a = Nil. I'll see how this could be done later today.

Similarly, when it comes to has-one, I'd rather mark the attribute or its value with something recognizable that would allow Red to distinguish one kind of relationship from another. But this is just a side note, an idea to consider some day in the future. :)

@vrurg
Copy link
Contributor Author

vrurg commented Mar 10, 2021

Ok, the above example was a quick hack, a proof of concept. Here is what I came up with to support assignment of Nil and assignment in principle:

use nqp;
multi trait_mod:<is> (Attribute:D $a is raw, :$foo!) {
    my role ModHelper {
        method create(*%c) {
            say "CREATING WITH ", %c;
        }
    };
    if nqp::defined($a.container_descriptor.default) {
        $a.container_descriptor.default does ModHelper;
    } else {
        my $def := $a.container_descriptor.default but ModHelper;
        nqp::bindattr(
            nqp::decont($a.container_descriptor), ContainerDescriptor, '$!default', $def);
        nqp::bindattr($a.auto_viv_container, Scalar, '$!value', $def);
    }
}

class Foo {
    has Int $.a is rw is default(666) is foo;
}

my $foo = Foo.new;
note "attr on instance: ", $foo.a.WHICH;
$foo.a.create(:1a, :2b);
$foo.a = 42;
say "-> ", $foo.a;
$foo.a = Nil;
note $foo.a.WHICH;
$foo.a.create(:3c, :4d);

The interesting part of it is that it is actually possible set attribute type to the model it is refers to. I.e.:

model Foo {
    has $.attr is column(..., :model<MyModel>);
}

Foo.attr.WHICH # MyModel

@FCO
Copy link
Owner

FCO commented Mar 21, 2021

Closing this one and creating #475 to continue the .create discussion.

@FCO FCO closed this as completed Mar 21, 2021
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