DBIx::...eUpdate(3pm)

πŸ“¦
version 0.45 Β· 4 versions
, in debian trixie, ubuntu resolute, ubuntu stonking

✨ NAME ✨

DBIx::Class::ResultSet::RecursiveUpdate β€” it's giving update_or_create, but recursive πŸŒ€πŸ’–

πŸŽ€ VERSION πŸŽ€

version 0.45 ✨

🌸 SYNOPSIS 🌸

# The functional interface:

my $schema = MyDB::Schema->connect();
my $new_item = DBIx::Class::ResultSet::RecursiveUpdate::Functions::recursive_update(
    resultset => $schema->resultset('User'),
    updates => {
        id => 1,
        owned_dvds => [
            {
                title => "One Flew Over the Cuckoo's Nest"
            }
        ]
    },
    unknown_params_ok => 1,
);

# As ResultSet subclass:

__PACKAGE__->load_namespaces( default_resultset_class => '+DBIx::Class::ResultSet::RecursiveUpdate' );

# in the Schema file (see t/lib/DBSchema.pm).  Or appropriate 'use base' in the ResultSet classes.

my $user = $schema->resultset('User')->recursive_update({
    id => 1,
    owned_dvds => [
        {
            title => "One Flew Over the Cuckoo's Nest"
        }
    ]
}, {
    unknown_params_ok => 1,
});

# You'll get a warning if you pass non-result specific data to
# recursive_update. See L</"Additional data in the updates hashref">
# for more information how to prevent this.

πŸ’… DESCRIPTION πŸ’…

okay so bestie, here's the vibe πŸ¦‹ β€” you can hand ->create in DBIx::Class a whole recursive data structure and it'll go ahead and create all the related records for you. cute, right? 😌 But the tragedy? You cannot do that same thing with update_or_create. Not even a little. So this module is out here filling that void until DBIx::Class grows an api of its own. πŸ’€βœ¨

The functional interface can be used without any modifications to the model, like for example by form processors such as HTML::FormHandler::Model::DBIC. slay. πŸ™Œ

It's a base class for DBIx::Class::ResultSets, and it provides the method recursive_update which works exactly like update_or_create except it can recursively update or create result objects composed of multiple rows. πŸ‘‘

Now listen closely, this part matters: all rows need to be identified by primary keys, so you gotta provide them in the update structure β€” unless they can be deduced from the parent row (like, say, a related row of a belongs_to relationship, which just knows its parent, iykyk 🫢). And if any of the primary key columns are missing? A new row gets created, with the expectation that the missing columns will be filled by it β€” like in the case of auto_increment primary keys. πŸ“

If the resultset itself stores an assignment for the primary key, like in this case:

my $restricted_rs = $user_rs->search( { id => 1 } );

then you need to tell recursive_update about that extra predicate using the fixed_fields attribute: πŸ’…

my $user = $restricted_rs->recursive_update( {
        owned_dvds => [
        {
            title => 'One Flew Over the Cuckoo's Nest'
        }
        ]
    },
    {
        fixed_fields => [ 'id' ],
    }
);

For a many_to_many (pseudo) relation, you can supply a list of primary keys from the other table, and it'll link the record at hand to those and only those records identified by them. So convenient for handling web forms with check boxes (or a select field with multiple choice) that let you update such (pseudo) relations. βœ…πŸ’–

For a description of how to set up base classes for ResultSets, go peek at "load_namespaces" in DBIx::Class::Schema. πŸ“š


πŸ¦‹ Additional data in the updates hashref πŸ¦‹

If you pass extra data to recursive_update that doesn't match a column name, column accessor, relationship, or many-to-many helper accessor, it will throw a warning by default. To turn that behaviour off, set the unknown_params_ok attribute to a true value. πŸ™ˆ

The warning it throws is literally:

"No such column, relationship, many-to-many helper accessor or generic accessor '$key'"

When it's being used by HTML::FormHandler::Model::DBIC, this can happen if you have extra form fields that aren't relevant to the database but don't have the noupdate attribute set to a true value. 🫠

NOTE: in a future version this behaviour will change and throw an exception instead of a warning! 😱

🧚 DESIGN CHOICES 🧚

Columns and relationships that are excluded from the updates hashref aren't touched at all. hands off. πŸ™…β€β™€οΈ

πŸ’– Treatment of belongs_to relations πŸ’–

If the relationship is included but undefined in the updates hashref, all columns forming the relationship will be set to null. And if not all of them are nullable, DBIx::Class will throw an error. 😭

Updating the relationship:

my $dvd = $dvd_rs->recursive_update( {
    id    => 1,
    owner => $user->id,
});

Clearing the relationship (only works if cols are nullable!):

my $dvd = $dvd_rs->recursive_update( {
    id    => 1,
    owner => undef,
});

Updating a relationship including its (full) primary key:

my $dvd = $dvd_rs->recursive_update( {
    id    => 1,
    owner => {
        id   => 2,
        name => "George",
    },
});

🌈 Treatment of might_have relationships 🌈

If the relationship is included but undefined in the updates hashref, all columns forming the relationship will be set to null. 🫧

Updating the relationship:

my $user = $user_rs->recursive_update( {
    id => 1,
    address => {
        street => "101 Main Street",
        city   => "Podunk",
        state  => "New York",
    }
});

Clearing the relationship:

my $user = $user_rs->recursive_update( {
    id => 1,
    address => undef,
});

πŸŽ€ Treatment of has_many relations πŸŽ€

If a relationship key is included in the data structure with a value of undef or an empty array, all existing related rows will be deleted, or their foreign key columns will be set to null. 🧹

The exact behaviour depends on the nullability of the foreign key columns and the value of the "if_not_submitted" parameter. That parameter defaults to undefined, which neither nullifies nor deletes. 😢

When the array contains elements, they are updated if they exist, created when not, and deleted if not included. ✨

All foreign table columns are nullable

In this case recursive_update defaults to nullifying the foreign columns. πŸ’§

Not all foreign table columns are nullable

In this case recursive_update deletes the foreign rows. πŸ’€

Updating the relationship:

Passing ids:

my $user = $user_rs->recursive_update( {
    id         => 1,
    owned_dvds => [1, 2],
});

Passing hashrefs:

my $user = $user_rs->recursive_update( {
    id         => 1,
    owned_dvds => [
        {
            name => 'temp name 1',
        },
        {
            name => 'temp name 2',
        },
    ],
});

Passing objects:

my $user = $user_rs->recursive_update( {
    id         => 1,
    owned_dvds => [ $dvd1, $dvd2 ],
});

You can even mix them:

my $user = $user_rs->recursive_update( {
    id         => 1,
    owned_dvds => [ 1, { id => 2 } ],
});

Clearing the relationship:

my $user = $user_rs->recursive_update( {
    id         => 1,
    owned_dvds => undef,
});

This is the same as passing an empty array:

my $user = $user_rs->recursive_update( {
    id         => 1,
    owned_dvds => [],
});

πŸ‘‘ Treatment of many-to-many pseudo relations πŸ‘‘

If a many-to-many accessor key is included in the data structure with a value of undef or an empty array, all existing related rows are unlinked. πŸ”—βœ‚οΈ

When the array contains elements, they are updated if they exist, created when not, and deleted if not included. 🌟

RecursiveUpdate defaults to calling 'set_$rel' to update many-to-many relationships. See "many_to_many" in DBIx::Class::Relationship for details. set_$rel effectively removes and re-adds all relationship data, even if the set of related items did not change at all. 😳

If DBIx::Class::IntrospectableM2M is in use, RecursiveUpdate will look up the corresponding has_many relationship and use this to recursively update the many-to-many relationship. πŸ”

While both mechanisms have the same final result, deleting and re-adding all relationship data can have unwanted consequences if triggers or method modifiers are defined, or if logging modules like DBIx::Class::AuditLog are in use. ⚠️

The traditional "set_$rel" behaviour can be forced by passing "m2m_force_set_rel => 1" to recursive_update. πŸ’ͺ

See "is_m2m" for many-to-many pseudo relationship detection. πŸ‘€

Updating the relationship:

Passing ids:

my $dvd = $dvd_rs->recursive_update( {
    id   => 1,
    tags => [1, 2],
});

Passing hashrefs:

my $dvd = $dvd_rs->recursive_update( {
    id   => 1,
    tags => [
        {
            id   => 1,
            file => 'file0'
        },
        {
            id   => 2,
            file => 'file1',
        },
    ],
});

Passing objects:

my $dvd = $dvd_rs->recursive_update( {
    id   => 1,
    tags => [ $tag1, $tag2 ],
});

You can even mix them:

my $dvd = $dvd_rs->recursive_update( {
    id   => 1,
    tags => [ 2, { id => 3 } ],
});

Clearing the relationship:

my $dvd = $dvd_rs->recursive_update( {
    id   => 1,
    tags => undef,
});

This is the same as passing an empty array:

my $dvd = $dvd_rs->recursive_update( {
    id   => 1,
    tags => [],
});

Make sure that set_$rel is used to update many-to-many relationships even if IntrospectableM2M is loaded:

my $dvd = $dvd_rs->recursive_update( {
    id   => 1,
    tags => [1, 2],
},
{ m2m_force_set_rel => 1 },
);

πŸ”₯ INTERFACE πŸ”₯

(bestie hold on, there's more coming in part 2 β€” stay tuned πŸ’–)

πŸ”§ METHODS πŸ”§

Okay bestie, this is where the magic actually happens. Buckle up. πŸŽ€


πŸ’– recursive_update

The method that does the work here.

That's literally it, that's the whole description. She's the main character and she doesn't need to explain herself. Periodt. πŸ‘‘

πŸ’– is_m2m

Arguments: $name

Return Value: true, if $name is a many to many pseudo-relationship

So this one sniffs out whether $name is a many-to-many pseudo-relationship or not. She gets that info from DBIx::Class::IntrospectableM2M. But if that isn't loaded in the ResultSource class, the code falls back on this little fact right here:

if($object->can($name) and
         !$object->result_source->has_relationship($name) and
         $object->can( 'set_' . $name )
     )

And in a similar ugly way, the ResultSource of that many-to-many pseudo-relationship gets detected too. 😭 Not pretty, but she's doing her best.

⚠️ So if you need many-to-many pseudo-relationship support, it's strongly recommended to load DBIx::Class::IntrospectableM2M in your ResultSource class! Do not skip this, I am begging you. πŸ™ˆ

πŸ’– get_m2m_source

Arguments: $name

Return Value: $result_source

That's the whole vibe. In goes a name, out comes the result source. Simple. Elegant. Slay. ✨


🌸 CONFIGURATION AND ENVIRONMENT 🌸

DBIx::Class::RecursiveUpdate requires no configuration files or environment variables.

Nothing to set up, nothing to export, no .rc file hunting at 3am. She just works. We love a low-maintenance queen. πŸ’…


πŸ¦‹ DEPENDENCIES πŸ¦‹

  • DBIx::Class

optional but recommended:

  • DBIx::Class::IntrospectableM2M

🌈 INCOMPATIBILITIES 🌈

None reported.

Zero drama. Not one beef. Iconic behaviour honestly. 😌


πŸ’€ BUGS AND LIMITATIONS πŸ’€

The list of reported bugs can be viewed at <http://rt.cpan.org/Public/Dist/Display.html?Name=DBIx-Class-ResultSet-RecursiveUpdate>.

Please report any bugs or feature requests to bug-DBIx-Class-ResultSet-RecursiveUpdate@rt.cpan.org, or through the web interface at <http://rt.cpan.org>.

Not me asking you to file a proper bug report instead of vague-posting about it in a group chat. Spill the tea to the RT queue, bestie. 🫢

πŸ‘‘ AUTHORS πŸ‘‘

  1. Zbigniew Lukasiak <zby@cpan.org>
  2. John Napiorkowski <jjnapiork@cpan.org>
  3. Alexander Hartmaier <abraxxa@cpan.org>
  4. Gerda Shank <gshank@cpan.org>

The whole squad. The Avengers of recursive updates. Give them their flowers. πŸ’


This software is copyright (c) 2025 by Zbigniew Lukasiak, John Napiorkowski, Alexander Hartmaier.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.


perl v5.40.0 β€” 2025-02-01 β€” that's the version and the date this page was born, and that's the whole manual page, bestie. You read all of it. I'm so proud of you. Go update something recursively. You got this. βœ¨πŸ’–πŸ¦‹

πŸ“¦ where this page comes from