0

Update I normally roam in the latex part of stachexchange and thus we have to provide a full minimal example to reproduce issues.

So here is a full breakdown. The original description of my problem is found below.

Test DB setup, we using SQLite, create.sql:

PRAGMA foreign_keys = ON;

DROP TABLE IF EXISTS `member`; 
DROP TABLE IF EXISTS `address`; 

create table `member` (
       `uid`    VARCHAR(30)  NOT NULL,
       `name`   VARCHAR(255) DEFAULT '',
       CONSTRAINT `pk_uid` PRIMARY KEY(`uid`)
);

INSERT INTO `member` VALUES ('m1','Test 1'),('m2','Test 2');

create table `address` (
       `uid`   VARCHAR(30)  NOT NULL,
       `address_type`  VARCHAR(30)  NOT NULL, -- will either be work or home
       `text`  TEXT         DEFAULT '',
       CONSTRAINT `pk_uid_type` UNIQUE(`uid`,`address_type`)
       CONSTRAINT `fk_uid`
         FOREIGN KEY(uid)
     REFERENCES member(uid)
         ON DELETE CASCADE
);

INSERT INTO `address` VALUES
('m1','home','home address'),
('m1','work','work address'),
('m2','home','home address');

to be loaded into test.db via

sqlite3 test.db < create.sql

As we can see from the test data m1 has two entries in address whereas m2 has one.

Next the DBIx setup (I have no idea how to merge this into a single file, ideas a welcome as it would making the test easier). These are autogenerated via dbicdump, here I've removed alle the comments.

Schema.pm:

use utf8;
package Schema;
use strict;
use warnings;
use base 'DBIx::Class::Schema';
__PACKAGE__->load_namespaces;
1;

Schema/Result/Member.pm:

use utf8;
package Schema::Result::Member;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("member");
__PACKAGE__->add_columns(
  "uid",
  { data_type => "varchar", is_nullable => 0, size => 30 },
  "name",
  { data_type => "varchar", default_value => "", is_nullable => 1, size => 255 },
);
__PACKAGE__->set_primary_key("uid");
__PACKAGE__->has_many(
  "addresses",
  "Schema::Result::Address",
  { "foreign.uid" => "self.uid" },
  { cascade_copy => 0, cascade_delete => 0 },
);

# I added

__PACKAGE__->might_have(
    "home_address" =>  "Schema::Result::Address",
    #{ 'foreign.uid' => 'self.uid'},
    sub {
    my $args = shift;
    return {
        "$args->{foreign_alias}.uid" => "$args->{self_alias}.uid",
        "$args->{foreign_alias}.address_type"   => 'home',
      }
    },
    {  cascade_copy => 0, cascade_delete => 0 },
);

__PACKAGE__->might_have(
    "home_address_alt" =>  "Schema::Result::Address",
    { 'foreign.uid' => 'self.uid'},
    {  cascade_copy => 0, cascade_delete => 0 },
);

__PACKAGE__->might_have(
    "work_address" =>  "Schema::Result::Address",
    sub {
    my $args = shift;
    return {
        "$args->{foreign_alias}.uid" => "$args->{self_alias}.uid",
        "$args->{foreign_alias}.address_type"   => 'work',
      }
    },
    {  cascade_copy => 0, cascade_delete => 0 },
);

1;

Schema/Result/Address.pm:

use utf8;
package Schema::Result::Address;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("address");
__PACKAGE__->add_columns(
  "uid",
  { data_type => "varchar", is_foreign_key => 1, is_nullable => 0, size => 30 },
  "address_type",
  { data_type => "varchar", is_nullable => 0, size => 30 },
  "text",
  { data_type => "text", default_value => "", is_nullable => 1 },
);
__PACKAGE__->add_unique_constraint("uid_address_type_unique", ["uid", "address_type"]);
__PACKAGE__->belongs_to(
  "u",
  "Schema::Result::Member",
  { uid => "uid" },
  { is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" },
);

1;

My test script:

#!/usr/bin/perl

use strict;
use warnings;
use utf8;
use open qw/:std :utf8/;
use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Maxdepth = 0;
use Modern::Perl;
use lib qw(.);
use Schema;

BEGIN {
    $ENV{DBIC_TRACE} = 1;
}


my $schema = Schema->connect(
  'dbi:SQLite:dbname=test.db',
  '',
  '',
  {
      on_connect_do => 'PRAGMA foreign_keys = ON',
      sqlite_unicode =>  1,
      RaiseError => 1,
  }
); 

my $row = $schema->resultset('Member')->find({ uid => 'm1'},
                         {
                         prefetch => ['home_address','work_address'],
                         }
                     );
# these are both undef
print Dumper $row->home_address;
print Dumper $row->work_address;

# using
$row = $schema->resultset('Member')->find({ uid => 'm1'},
                      {
                          prefetch => ['home_address','work_address'],
                          result_class => 'DBIx::Class::ResultClass::HashRefInflator',
                      }
                      );

# then
print Dumper $row;
# gives
# $VAR1 = {
#           'home_address' => undef,
#           'name' => 'Test 1',
#           'uid' => 'm1',
#           'work_address' => undef
#         };


# using the "normal might_have home_address_alt in Member on m2

$row = $schema->resultset('Member')->find({ uid => 'm2'},
                      {
                          prefetch => ['home_address_alt'],
                          result_class => 'DBIx::Class::ResultClass::HashRefInflator',
                      }
                      );

say Dumper $row;
# does work, but only because m2 only have a single entry in Address whereas m1 has two

$row = $schema->resultset('Member')->find({ uid => 'm1'},
                      {
                          prefetch => ['home_address_alt'],
                          result_class => 'DBIx::Class::ResultClass::HashRefInflator',
                      }
                      );

say Dumper $row;

# which gives this warning: DBIx::Class::Storage::DBI::select_single(): Query returned more than one row.  SQL that returns multiple rows is DEPRECATED for ->find and ->single and returns the first found.

The DBIC_TRACE gives

SELECT me.uid, me.name, home_address.uid, home_address.address_type, home_address.text, work_address.uid, work_address.address_type, work_address.text FROM member me LEFT JOIN address home_address ON ( home_address.address_type = ? AND home_address.uid = ? ) LEFT JOIN address work_address ON ( work_address.address_type = ? AND work_address.uid = ? ) WHERE ( me.uid = ? ): 'home', 'me.uid', 'work', 'me.uid', 'm1'

Which if you run it manually against test.db gives

m1|Test 1|m1|home|home address|m1|work|work address

So the SQL is capable of producing the correct output. But the accessors/objects whatever you want to call them, keeps being empty. I'd like to know why?


My original question:

I my data I have members and they can each have up to two addresses (home and work) both stored in the same table

So I have something similar to

Member; primary key(uid)
Address; unique(uid,address_type) # the latter is work or home
 

when I grab a member I'd like to prefetch the up to two addresses using might_have relationships. So in Schema::Result::Member I have

__PACKAGE__->might_have(
    "home_address" =>  "Schema::Result::Address",
    sub {
    my $args = shift;
    return {
        "$args->{foreign_alias}.uid" => "$args->{self_alias}.uid",
        "$args->{foreign_alias}.address_type"   => 'home',
      }
    },
    {  cascade_copy => 0, cascade_delete => 0 },
);
__PACKAGE__->might_have(
    "work_address" =>  "Schema::Result::Address",
    sub {
    my $args = shift;
    return {
        "$args->{foreign_alias}.uid" => "$args->{self_alias}.uid",
        "$args->{foreign_alias}.address_type"   => 'work',
      }
    },
    {  cascade_copy => 0, cascade_delete => 0 },
);

And I call it via

my $row = $self->schema->resultset('Member')
    ->find({uid => $uid},
           {
           prefetch => [qw/home_address work_address/],
           });

As far as I can see from DBIC_TRACE the generated SQL is correct

... LEFT JOIN address home_address ON ( home_address.address_type = ? AND home_address.uid = ? ) LEFT JOIN address work_address ON ( work_address.address_type = ? AND work_address.uid = ? ) WHERE ( me.uid = ? ): 'home', 'me.uid', 'work', 'me.uid', '120969'

but $row->home_address is always just undef and I do not understand why.

I have also tried

__PACKAGE__->might_have(
  "home_address" => "Schema::Result::Address",
  { 'foreign.uid' => 'self.uid' },
  { where => { 'address_type' => 'home' } , cascade_copy => 0, cascade_delete => 0 },
);
__PACKAGE__->might_have(
  "work_address" =>  "Schema::Result::Address",
  { 'foreign.uid' => 'self.uid' },
  { where => { 'address_type' => 'work' } , cascade_copy => 0, cascade_delete => 0 },
);

but the where part is never a part of the DBIC_TRACE.

Any ideas as to what I'm missing here?

daleif
  • 217
  • 1
  • 10
  • Does the SQL query return what you expect when running it against your database? – Alexander Hartmaier Apr 06 '21 at 21:11
  • As far as I can tell yes. It seems something goes wrong in the objectification somewhere. Which is the part I don't understand. The test uid was chooses as it has both types of address data. And the SQL does return both – daleif Apr 06 '21 at 21:14
  • To circumvent the objectification you can use HashRefInflator. – Alexander Hartmaier Apr 07 '21 at 06:14
  • @AlexanderHartmaier I'll try that, but it doesn't explain why there is no object. I have the same issue in another app where I need to use sub to make a very specific condition. Again the SQL seems to give the correct result, but the object for it is empty. So either I'm misunderstanding something or the manual is not thorough enough – daleif Apr 07 '21 at 06:38
  • @AlexanderHartmaier tried `result_class => 'DBIx::Class::ResultClass::HashRefInflator'` on a find. This also just leaves `home_address` and `work_address` empty. I'll check some versions. – daleif Apr 07 '21 at 15:50
  • Can you paste some example table data, the generated query and the rows the query returns when run against the database without DBIC? – Alexander Hartmaier Apr 07 '21 at 19:07
  • @AlexanderHartmaier I was writing a full minimal example including dB etc at work before I went home. I'll finish it and post it tomorrow – daleif Apr 07 '21 at 19:09
  • @AlexanderHartmaier added a full example, including sample SQL, output from DBIC_TRACE output from actually trying that SQL. Any ideas? – daleif Apr 08 '21 at 07:55
  • It does seem to be related to using a callback to make the condition part. I added an extra table to the example where the data for `m1` and `m2` is uniq. The normal autogenerated `might_have` works just fine. But that ought to be the same as using `sub { my $args = shift; return { "$args->{foreign_alias}.uid" => "$args->{self_alias}.uid" } },` but that also gets undef data out. So the issue seems to be here. }, – daleif Apr 08 '21 at 08:37

1 Answers1

4

The DBIx::Class docs have an example of a custom relationship with fixed values on the remote side of the rel: https://metacpan.org/pod/DBIx::Class::Relationship::Base#Custom-join-conditions.

The part you've missed is the -ident, so DBIC can distinguish between a fixed value and a related column. Because of that the query ends up with a bind variable that is passed the literal string 'me.uid' on execution.

Alexander Hartmaier
  • 2,178
  • 12
  • 21
  • Nice catch, that works. Did indeed completely overlook `-ident`. I'll of this also fixes a similar problem I have in a long prefetch chain, where in the very last level I additionally want to refer to the `me.uid` on the very outer level. – daleif Apr 08 '21 at 12:23
  • It does work there as well (been struggiling with that one for a week), but I have to hardcode `me.something` because I have to refer to the `me` that is the stuff we are joining to. We can probably access this through the `args` variable somehow. But important this is this actually works now and will save be a lot of time. – daleif Apr 08 '21 at 12:34
  • DBIC will add the correct identifier when using -ident as far as I know. – Alexander Hartmaier Apr 08 '21 at 16:20
  • I got an error when using `$args->{self_alias}.uid` but explicitly `me.uid` worked perfectly when `-ident` was added. – daleif Apr 08 '21 at 16:22
  • Note that this was for a different problem Short version: have two data tables `A` and `C`, and to tables that relates these two `chosen` and `assigned`. And I was going this route `C->chosen->A->assigned` (-> = joins). In the `A->assigned` part I of course wanted to look at the current element in C namely the actual `me.cid`, if we use `$args->{self_alias}.cid` in the `might_have on `A` then we get an error as `$args->self_alias}.cid` will refer to `A` which does not have any `cid` (it has `aid`). But hardcoding `me.cid` works. Hope it was understandable. This missing `-ident` solved a lot – daleif Apr 09 '21 at 07:53
  • Hmm, interesting observation in the `C->chosen->A->assigned` I actually have two `has_many` made this way using the same name!! And it only works if I have both. Hmm. – daleif Apr 09 '21 at 12:31