Skip Menu |

This queue is for tickets about the DBIx-Class CPAN distribution.

Report information
The Basics
Id: 114493
Status: open
Priority: 0/
Queue: DBIx-Class

People
Owner: Nobody in particular
Requestors: pushtaev [...] cpan.org
Cc:
AdminCc:

Bug Information
Severity: (no value)
Broken in: 0.082821
Fixed in: (no value)



Subject: Deep recursive create problem
Hello. What I'm trying to do is to create several related entities with a single "create" call: $schema->resultset('Account')->create({ sum => 42, user => { superuser => 0, name => 'vadim', city => {name => 'Moscow'}, }, }) For some reason DBIx::Class tries to find "user" and "city" before creating new ones. I consider this a feature (though I would prefer to also have a pure deep recursive "create"), but there is a still a problem with it. Do determine whether such user already exists, DBIx::Class searches for {superuser=>0, name=>'vadim'} ignoring the city completely. This is where it happens: DBIx/Class/Row.pm:134 my $rel_rs = $rsrc->related_source($rel_name)->resultset; my $new_rel_obj = $rel_rs->new_result($values); my $proc_data = { $new_rel_obj->get_columns }; We lose {city => ...} here. As a result we can find some another user with some another city or even several such users (which resuls in a warning, because "find" should always find a single row). So my questions are: 1. Do we really need this "find or create" behavior? 2. How do I turn it off? 3. Is it even possible to find a row according to the data what is going to be inserted? I maybe want to insert user with {name => "..."} and other fields being filled by the database defaults (and that doesn't mean at all any user with name="..." is ok for me). 4. Is this thing I discovered a bug? How do I fix it? The full working example is here: use strict; use warnings; { package Local::Schema::City; use base qw(DBIx::Class); __PACKAGE__->load_components('Core'); __PACKAGE__->table('city'); __PACKAGE__->add_columns( id => { data_type => 'integer', is_auto_increment => 1, }, name => { data_type => 'varchar', size => '100', }, comment => { data_type => 'varchar', size => '100', default_value => 'DEFAULT COMMENT', is_nullable => 1, }, ); __PACKAGE__->set_primary_key('id'); __PACKAGE__->has_many( users => 'Local::Schema::User', 'city_id' ); } { package Local::Schema::User; use base qw(DBIx::Class); __PACKAGE__->load_components('Core'); __PACKAGE__->table('user'); __PACKAGE__->add_columns( id => { data_type => 'integer', is_auto_increment => 1, }, name => { data_type => 'varchar', size => '100', }, comment => { data_type => 'varchar', size => '100', default_value => 'DEFAULT COMMENT', is_nullable => 1, }, superuser => { data_type => 'bool', }, city_id => { data_type => 'integer', }, ); __PACKAGE__->set_primary_key('id'); __PACKAGE__->has_many( accounts => 'Local::Schema::Account', 'user_id' ); __PACKAGE__->belongs_to( city => 'Local::Schema::City', 'city_id' ); } { package Local::Schema::Account; use base qw(DBIx::Class); __PACKAGE__->load_components('Core'); __PACKAGE__->table('account'); __PACKAGE__->add_columns( id => { data_type => 'integer', is_auto_increment => 1, }, sum => { data_type => 'integer', }, user_id => { data_type => 'integer', }, ); __PACKAGE__->set_primary_key('id'); __PACKAGE__->belongs_to( user => 'Local::Schema::User', 'user_id' ); } { package Local::Schema; use base qw(DBIx::Class::Schema); __PACKAGE__->load_classes('City', 'User', 'Account'); } my $schema = Local::Schema->connect( 'dbi:SQLite:dbname=local.sqlite', '', '' ); $schema->storage->debug(0); $schema->deploy(); =uncomment to see warnings my $city = $schema->resultset('City')->create({name => 'C'}); $schema->resultset('User')->create({superuser => 0, name => 'vadim', city => $city}); $schema->resultset('User')->create({superuser => 0, name => 'vadim', city => $city}); $schema->resultset('User')->create({superuser => 0, name => 'vadim', city => $city}); =cut print $schema->resultset('Account')->create({ sum => 42, user => { superuser => 0, name => 'vadim', city => {name => 'Moscow'}, }, })->user->id; print $schema->resultset('Account')->create({ sum => 42, user => { superuser => 0, name => 'vadim', city => {name => 'London'}, }, })->user->id; # same ID :( END { unlink('local.sqlite'); }
Oh, I've found this: CAVEAT: The behavior described above will backfire if you use a foreign key column with a database-defined default. If you call the relationship accessor on an object that doesn't have a set value for the FK column, DBIC will throw an exception, as it has no way of knowing the PK of the related object (if there is one). It still doesn't explain why we don't look for "city" though. On Thu May 19 11:35:59 2016, PUSHTAEV wrote: Show quoted text
> Hello. > > What I'm trying to do is to create several related entities with a > single "create" call: > > $schema->resultset('Account')->create({ > sum => 42, > user => { > superuser => 0, > name => 'vadim', > city => {name => 'Moscow'}, > }, > }) > > For some reason DBIx::Class tries to find "user" and "city" before > creating new ones. I consider this a feature (though I would prefer to > also have a pure deep recursive "create"), but there is a still a > problem with it. > > Do determine whether such user already exists, DBIx::Class searches > for {superuser=>0, name=>'vadim'} ignoring the city completely. This > is where it happens: > > DBIx/Class/Row.pm:134 > > my $rel_rs = $rsrc->related_source($rel_name)->resultset; > my $new_rel_obj = $rel_rs->new_result($values); > my $proc_data = { $new_rel_obj->get_columns }; > > We lose {city => ...} here. As a result we can find some another user > with some another city or even several such users (which resuls in a > warning, because "find" should always find a single row). > > So my questions are: > > 1. Do we really need this "find or create" behavior? > 2. How do I turn it off? > 3. Is it even possible to find a row according to the data what is > going to be inserted? I maybe want to insert user with {name => "..."} > and other fields being filled by the database defaults (and that > doesn't mean at all any user with name="..." is ok for me). > 4. Is this thing I discovered a bug? How do I fix it? > > The full working example is here: > > use strict; > use warnings; > > { > package Local::Schema::City; > use base qw(DBIx::Class); > > __PACKAGE__->load_components('Core'); > __PACKAGE__->table('city'); > __PACKAGE__->add_columns( > id => { > data_type => 'integer', > is_auto_increment => 1, > }, > name => { > data_type => 'varchar', > size => '100', > }, > comment => { > data_type => 'varchar', > size => '100', > default_value => 'DEFAULT COMMENT', > is_nullable => 1, > }, > ); > __PACKAGE__->set_primary_key('id'); > __PACKAGE__->has_many( > users => 'Local::Schema::User', > 'city_id' > ); > } > > { > package Local::Schema::User; > use base qw(DBIx::Class); > > __PACKAGE__->load_components('Core'); > __PACKAGE__->table('user'); > __PACKAGE__->add_columns( > id => { > data_type => 'integer', > is_auto_increment => 1, > }, > name => { > data_type => 'varchar', > size => '100', > }, > comment => { > data_type => 'varchar', > size => '100', > default_value => 'DEFAULT COMMENT', > is_nullable => 1, > }, > superuser => { > data_type => 'bool', > }, > city_id => { > data_type => 'integer', > }, > ); > __PACKAGE__->set_primary_key('id'); > __PACKAGE__->has_many( > accounts => 'Local::Schema::Account', > 'user_id' > ); > __PACKAGE__->belongs_to( > city => 'Local::Schema::City', > 'city_id' > ); > } > > { > package Local::Schema::Account; > use base qw(DBIx::Class); > > __PACKAGE__->load_components('Core'); > __PACKAGE__->table('account'); > __PACKAGE__->add_columns( > id => { > data_type => 'integer', > is_auto_increment => 1, > }, > sum => { > data_type => 'integer', > }, > user_id => { > data_type => 'integer', > }, > ); > __PACKAGE__->set_primary_key('id'); > __PACKAGE__->belongs_to( > user => 'Local::Schema::User', > 'user_id' > ); > } > > { > package Local::Schema; > use base qw(DBIx::Class::Schema); > > __PACKAGE__->load_classes('City', 'User', 'Account'); > } > > my $schema = Local::Schema->connect( > 'dbi:SQLite:dbname=local.sqlite', '', '' > ); > $schema->storage->debug(0); > $schema->deploy(); > > =uncomment to see warnings > my $city = $schema->resultset('City')->create({name => 'C'}); > $schema->resultset('User')->create({superuser => 0, name => 'vadim', > city => $city}); > $schema->resultset('User')->create({superuser => 0, name => 'vadim', > city => $city}); > $schema->resultset('User')->create({superuser => 0, name => 'vadim', > city => $city}); > =cut > > print $schema->resultset('Account')->create({ > sum => 42, > user => { > superuser => 0, > name => 'vadim', > city => {name => 'Moscow'}, > }, > })->user->id; > > print $schema->resultset('Account')->create({ > sum => 42, > user => { > superuser => 0, > name => 'vadim', > city => {name => 'London'}, > }, > })->user->id; # same ID :( > > END { > unlink('local.sqlite'); > }
On Thu May 19 17:35:59 2016, PUSHTAEV wrote: Show quoted text
> Hello. > > What I'm trying to do is to create several related entities with a > single "create" call: > > $schema->resultset('Account')->create({ > sum => 42, > user => { > superuser => 0, > name => 'vadim', > city => {name => 'Moscow'}, > }, > }) > > For some reason DBIx::Class tries to find "user" and "city" before > creating new ones.
It does so because both relationships in this direction are can_not_exist_without => depending_on, which by definition is X:1. This means that without the find-first step, there would be no way to create several accounts for the same user or several users in the same city (the second same-user or same-city creation would explode). Show quoted text
> I consider this a feature (though I would prefer to > also have a pure deep recursive "create"), but there is a still a > problem with it. >
See above why "pure deep recursive" can't work conceptually. Show quoted text
> Do determine whether such user already exists, DBIx::Class searches > for {superuser=>0, name=>'vadim'} ignoring the city completely. This > is where it happens: > > DBIx/Class/Row.pm:134 > > my $rel_rs = $rsrc->related_source($rel_name)->resultset; > my $new_rel_obj = $rel_rs->new_result($values); > my $proc_data = { $new_rel_obj->get_columns }; > > We lose {city => ...} here. As a result we can find some another user > with some another city or even several such users (which resuls in a > warning, because "find" should always find a single row). > > So my questions are: > > 1. Do we really need this "find or create" behavior?
Yes. Show quoted text
> 2. How do I turn it off?
You can't. Show quoted text
> 3. Is it even possible to find a row according to the data what is > going to be inserted? I maybe want to insert user with {name => "..."} > and other fields being filled by the database defaults (and that > doesn't mean at all any user with name="..." is ok for me).
This is a limitation of how find() was designed at the beginning - it will work based on the first thing that satisfies a unique constraint. Given city is not part of any UC - it won't be considered :/ Show quoted text
> 4. Is this thing I discovered a bug? How do I fix it? >
It is a bug in terms of "behavior does not entirely make sense". I do not have an immediate answer on how to fix it, or whether a fix is possible at all. I might circle back to this in several days, but please do share other ideas you can come up with after reading the above explanation.
Thanks for the reply. On Thu May 26 12:02:03 2016, RIBASUSHI wrote: Show quoted text
> On Thu May 19 17:35:59 2016, PUSHTAEV wrote:
> > Hello. > > > > What I'm trying to do is to create several related entities with a > > single "create" call: > > > > $schema->resultset('Account')->create({ > > sum => 42, > > user => { > > superuser => 0, > > name => 'vadim', > > city => {name => 'Moscow'}, > > }, > > }) > > > > For some reason DBIx::Class tries to find "user" and "city" before > > creating new ones.
> > It does so because both relationships in this direction are > can_not_exist_without => depending_on, which by definition is X:1. > This means that without the find-first step, there would be no way to > create several accounts for the same user or several users in the same > city (the second same-user or same-city creation would explode). >
> > I consider this a feature (though I would prefer to > > also have a pure deep recursive "create"), but there is a still a > > problem with it. > >
> > See above why "pure deep recursive" can't work conceptually.
Well, I believed (and I was wrong as I can see now) that if you use hashref, it means you want to actually *insert* something. But in case you want to use something existing, you just use DBIx::Class::Row instead. So to create several accounts for the same user you just do the following: my $user = $schema->resultset('User')->create({...}); my $account1 = $schema->resultset('Account')->create({user => $user, ...}); my $account2 = $schema->resultset('Account')->create({user => $user, ...}); Show quoted text
>
> > Do determine whether such user already exists, DBIx::Class searches > > for {superuser=>0, name=>'vadim'} ignoring the city completely. This > > is where it happens: > > > > DBIx/Class/Row.pm:134 > > > > my $rel_rs = $rsrc->related_source($rel_name)->resultset; > > my $new_rel_obj = $rel_rs->new_result($values); > > my $proc_data = { $new_rel_obj->get_columns }; > > > > We lose {city => ...} here. As a result we can find some another user > > with some another city or even several such users (which resuls in a > > warning, because "find" should always find a single row). > > > > So my questions are: > > > > 1. Do we really need this "find or create" behavior?
> > Yes. >
> > 2. How do I turn it off?
> > You can't. >
> > 3. Is it even possible to find a row according to the data what is > > going to be inserted? I maybe want to insert user with {name => > > "..."} > > and other fields being filled by the database defaults (and that > > doesn't mean at all any user with name="..." is ok for me).
> > This is a limitation of how find() was designed at the beginning - it > will work based on the first thing that satisfies a unique constraint. > Given city is not part of any UC - it won't be considered :/
But 'name' and 'superuser' aren't part of any UC either but they still *are* considered. Show quoted text
>
> > 4. Is this thing I discovered a bug? How do I fix it? > >
> > It is a bug in terms of "behavior does not entirely make sense". I do > not have an immediate answer on how to fix it, or whether a fix is > possible at all. I might circle back to this in several days, but > please do share other ideas you can come up with after reading the > above explanation.
My current workaround, by the way, is to specify {id => undef} explicitly to let DBIC know I don't want exactly new row here. However that isn't general solution. For example, it won't work if you have no 'id' in your table (or any other unique constraints).