package DBIx::EAV::ResultSet; use Moo; use DBIx::EAV::Entity; use DBIx::EAV::Cursor; use Data::Dumper; use Carp qw/ croak confess /; use overload '0+' => "_to_num", 'bool' => "_to_bool", fallback => 1; my $sql = SQL::Abstract->new; has 'eav', is => 'ro', required => 1; has 'type', is => 'ro', required => 1; has '_query', is => 'rw', default => sub { [] }, init_arg => 'query'; has '_options', is => 'rw', default => sub { {} }, init_arg => 'options'; has 'cursor', is => 'rw', lazy => 1, init_arg => undef, predicate => '_has_cursor', clearer => '_clear_cursor', builder => '_build_cursor'; sub _to_num { $_[0]->count } sub _to_bool { 1 } sub _build_cursor { my $self = shift; DBIx::EAV::Cursor->new( eav => $self->eav, type => $self->type, query => $self->_query, options => $self->_options, ); } sub new_entity { my ($self, $data) = @_; my $entity = DBIx::EAV::Entity->new( eav => $self->eav, type => $self->type ); $entity->set($data) if ref $data eq 'HASH'; $entity; } sub inflate_entity { my ($self, $data) = @_; my $type = $self->type; $type = $self->eav->type_by_id($data->{entity_type_id}) if $data->{entity_type_id} && $data->{entity_type_id} != $type->id; my $entity = DBIx::EAV::Entity->new( eav => $self->eav, type => $type, raw => $data ); $entity->load_attributes; $entity; } sub insert { my ($self, $data) = @_; $self->new_entity($data)->save; } sub populate { my ($self, $data) = @_; die 'Call populate(\@items)' unless ref $data eq 'ARRAY'; my @result; foreach my $item (@$data) { push @result, $self->insert($item); } return wantarray ? @result : \@result; } sub update { my ($self, $data, $where) = @_; $where //= {}; $where->{entity_type_id} = $self->type->id; # do a direct update for static attributes } sub delete { my $self = shift; my $eav = $self->eav; my $type = $self->type; my $entities_table = $eav->table('entities'); # Call delete_all for SQLite since it doesn't # support delete with joins. # Better solution welcome. return $self->delete_all if $self->eav->schema->db_driver_name eq 'SQLite'; unless ($eav->schema->database_cascade_delete) { # delete links by relationship id my @ids = map { $_->{id} } $type->relationships; $eav->table('entity_relationships')->delete( { relationship_id => \@ids, $entities_table->name.'.entity_type_id' => $type->id }, { join => { $entities_table->name => [{ 'me.left_entity_id' => 'their.id' }, { 'me.right_entity_id' => 'their.id' }] } } ); # delete attributes: # - group attrs by data type so only one DELETE command is sent per data type # - restrict by entity_type_id so we dont delete parent/sibiling/child data my %types; push @{ $types{$_->{data_type}} }, $_->{id} for $type->attributes(no_static => 1); while (my ($data_type, $ids) = each %types) { my $value_table = $eav->table('value_'.$data_type); $value_table->delete( { attribute_id => $ids, $entities_table->name.'.entity_type_id' => $type->id }, { join => { $entities_table->name => { 'me.entity_id' => 'their.id' } } } ); } } $entities_table->delete({ entity_type_id => $type->id }); } sub delete_all { my $self = shift; my $rs = scalar @_ > 0 ? $self->search_rs(@_) : $self; my $i = 0; while (my $entity = $rs->next) { $entity->delete; $i++; } $i; } sub find { my ($self, $criteria, $options) = @_; croak "Missing find() criteria." unless defined $criteria; # simple id search return $self->search_rs({ id => $criteria }, $options)->next unless ref $criteria; my $rs = $self->search_rs($criteria, $options); my $result = $rs->next; # criteria is a search query, die if this query returns multiple items croak "find() returned more than one entity. If this is what you want, use search or search_rs." if defined $result && defined $rs->cursor->next; $result; } sub search { my ($self, $query, $options) = @_; my $rs = $self->search_rs($query, $options); return wantarray ? $rs->all : $rs; } sub search_rs { my ($self, $query, $options) = @_; # simple combine queries using AND my @new_query = @{ $self->_query }; push @new_query, $query if $query; # merge options my $merged_options = $self->_merge_options($options); (ref $self)->new( eav => $self->eav, type => $self->type, query => \@new_query, options => $merged_options ); } sub _merge_options { my ($self, $options) = @_; my %merged = %{ $self->_options }; return \%merged unless defined $options; confess "WTF" if $options eq ''; foreach my $opt (keys %$options) { # doesnt even exist, just copy if (not exists $merged{$opt}) { $merged{$opt} = $options->{$opt}; } # having: combine queries using AND elsif ($opt eq 'having') { $merged{$opt} = [$merged{$opt}, $options->{$opt}]; } # merge array elsif (ref $merged{$opt} eq 'ARRAY') { $merged{$opt} = [ @{$merged{$opt}}, ref $options->{$opt} eq 'ARRAY' ? @{$options->{$opt}} : $options->{$opt} ]; } else { $merged{$opt} = $options->{$opt}; } } \%merged; } sub count { my $self = shift; return $self->search(@_)->count if @_; # from DBIx::Class::ResultSet::count() # this is a little optimization - it is faster to do the limit # adjustments in software, instead of a subquery my $options = $self->_options; my ($limit, $offset) = @$options{qw/ limit offset /}; my $count = $self->_count_rs($options)->cursor->next->{count}; $count -= $offset if $offset; $count = 0 if $count < 0; $count = $limit if $limit && $count > $limit; $count; } sub _count_rs { my ($self, $options) = @_; my %tmp_options = ( %$options, select => [\'COUNT(*) AS count'] ); # count using subselect if needed $tmp_options{from} = $self->as_query if $options->{group_by} || $options->{distinct}; delete @tmp_options{qw/ limit offset order_by group_by distinct /}; (ref $self)->new( eav => $self->eav, type => $self->type, query => [@{ $self->_query }], options => \%tmp_options ); } sub as_query { my $self = shift; $self->cursor->as_query; } sub reset { my $self = shift; $self->_clear_cursor; $self; } sub first { $_[0]->reset->next; } sub next { my $self = shift; # fetch next my $entity_row = $self->cursor->next; return unless defined $entity_row; # instantiate entity $self->inflate_entity($entity_row); } sub all { my $self = shift; my @entities; $self->reset; while (my $entity = $self->next) { push @entities, $entity; } $self->reset; return wantarray ? @entities : \@entities; } sub pager { die "pager() not implemented"; } sub distinct { die "distinct() not implemented"; } sub storage_size { die "storage_size() not implemented"; } 1; __END__ =encoding utf-8 =head1 NAME DBIx::EAV::ResultSet - Represents a query used for fetching a set of entities. =head1 SYNOPSIS # resultsets are bound to an entity type my $cds_rs = $eav->resultset('CD'); # insert CDs my $cd1 = $cds_rs->insert({ title => 'CD1', tracks => \@tracks }); my $cd2 = $cds_rs->insert({ title => 'CD2', tracks => \@tracks }); my $cd3 = $cds_rs->insert({ title => 'CD3', tracks => \@tracks }); # ... or use populate() to insert many my (@cds) = $cds_rs->populate(\@cds); # find all 2015 cds my @cds = $eav->resultset('CD')->search({ year => 2015 }); foreach my $cd (@cds) { printf "CD '%s' has %d tracks.\n", $cd->get('title'), $cd->get('tracks')->count; } # find one my $cd2 = $cds_rs->search_one({ name => 'CD2' }); # find by related attribute my $cd2 = $cds_rs->search_one({ 'tracks.title' => 'Some CD2 Track' }); # count my $top_cds_count = $cds_rs->search({ rating => { '>' => 7 } })->count; # update # delete all entities $cds_rs->delete; # fast, but doesn't deletes related entities $cds_rs->delete_all; # cascade delete all cds and related entities =head1 DESCRIPTION A ResultSet is an object which stores a set of conditions representing a query. It is the backbone of DBIx::EAV (i.e. the really important/useful bit). No SQL is executed on the database when a ResultSet is created, it just stores all the conditions needed to create the query. A basic ResultSet representing the data of an entire table is returned by calling C<resultset> on a L<DBIx::EAV> and passing in a L<type|DBIx::EntityType> name. my $users_rs = $eav->resultset('User'); A new ResultSet is returned from calling L</search> on an existing ResultSet. The new one will contain all the conditions of the original, plus any new conditions added in the C<search> call. A ResultSet also incorporates an implicit iterator. L</next> and L</reset> can be used to walk through all the L<entities|DBIx::EAV::Entity> the ResultSet represents. The query that the ResultSet represents is B<only> executed against the database when these methods are called: L</find>, L</next>, L</all>, L</first>, L</count>. If a resultset is used in a numeric context it returns the L</count>. However, if it is used in a boolean context it is B<always> true. So if you want to check if a resultset has any results, you must use C<if $rs != 0>. =head1 METHODS =head2 new_entity =over 4 =item Arguments: \%entity_data =item Return Value: L<$entity|DBIx::EAV::EntityType> =back Creates a new entity object of the resultset's L<type|DBIx::EAV::EntityType> and returns it. The row is not inserted into the database at this point, call L<DBIx::EAV::Entity/save> to do that. Calling L<DBIx::EAV::Entity/in_storage> will tell you whether the entity object has been inserted or not. # create a new entity, do some modifications... my $cd = $eav->resultset('CD')->new_entity({ title => 'CD1' }); $cd->set('year', 2016); # now insert it $cd->save; =head2 insert =over 4 =item Arguments: \%entity_data =item Return Value: L<$entity|DBIx:EAV::Entity> =back Attempt to create a single new entity or a entity with multiple related entities in the L<type|DBIx::EAV::EntityType> represented by the resultset (and related types). This will not check for duplicate entities before inserting, use L</find_or_create> to do that. To create one entity for this resultset, pass a hashref of key/value pairs representing the attributes of the L</type> and the values you wish to store. If the appropriate relationships are set up, you can also pass related data. To create related entities, pass a hashref of related-object attribute values B<keyed on the relationship name>. If the relationship is of type C<has_many> or C<many_to_many> - pass an arrayref of hashrefs. The process will correctly identify the relationship type and side, and will transparently populate the L<entitiy_relationships table>. This can be applied recursively, and will work correctly for a structure with an arbitrary depth and width, as long as the relationships actually exists and the correct data has been supplied. Instead of hashrefs of plain related data (key/value pairs), you may also pass new or inserted objects. New objects (not inserted yet, see L</new_entity>), will be inserted into their appropriate types. Effectively a shortcut for C<< ->new_entity(\%entity_data)->save >>. Example of creating a new entity. my $cd1 = $cds_rs->insert({ title => 'CD1', year => 2016 }); Example of creating a new entity and also creating entities in a related C<has_many> resultset. Note Arrayref for C<tracks>. my $cd1 = $eav->resultset('CD')->insert({ title => 'CD1', year => 2016 tracks => [ { title => 'Track1', duration => ... }, { title => 'Track2', duration => ... }, { title => 'Track3', duration => ... } ] }); Example of passing existing objects as related data. my @tags = $eav->resultset('Tag')->search(\%where); my $article = $eav->resultset('Article')->insert({ title => 'Some Article', content => '...', tags => \@tags }); =over =item WARNING When subclassing ResultSet never attempt to override this method. Since it is a simple shortcut for C<< $self->new_entity($data)->save >>, a lot of the internals simply never call it, so your override will be bypassed more often than not. Override either L<DBIx::EAV::Entity/new> or L<DBIx::EAV::Entity/save> depending on how early in the L</insert> process you need to intervene. =back =head2 populate =over 4 =item Arguments: \@entites =item Return Value: L<@inserted_entities|DBIx:EAV::Entity> =back Shortcut for inserting multiple entities at once. Returns a list of inserted entities. my @cds = $eav->resultset('CD')->populate([ { title => 'CD1', ... }, { title => 'CD2', ... }, { title => 'CD3', ... } ]); =head2 count =over 4 =item Arguments: \%where, \%options =item Return Value: $count =back Performs an SQL C<COUNT> with the same query as the resultset was built with to find the number of elements. Passing arguments is equivalent to C<< $rs->search($cond, \%attrs)->count >> =head2 delete =over 4 =item Arguments: \%where =item Return Value: $underlying_storage_rv =back Deletes the entities matching \%where condition without fetching them first. This will run faster, at the cost of related entities not being casdade deleted. Call L</delete_all> if you want to cascade delete related entities. When L<DBIx::EAV/database_cascade_delete> is enabled, the delete operation is done in a single query. Otherwise one more query is needed for each of the L<values table|DBIx::EAV::Schema> and another for the L<relationship link table|DBIx::EAV::Schema>. =over =item WARNING This method requires database support for C<DELETE ... JOIN>. Since the current implementation of DBIx::EAV is only tested against MySQL and SQLite, this method calls L</delete_all> if SQLite database is detected. =back =head2 delete_all =over 4 =item Arguments: \%where, \%options =item Return Value: $num_deleted =back Fetches all objects and deletes them one at a time via L<DBIx::EAV::Entity/delete>. Note that C<delete_all> will cascade delete related entities, while L</delete> will not. =head1 QUERY OPTIONS =head2 limit =over 4 =item Value: $rows =back Specifies the maximum number of rows for direct retrieval or the number of rows per page if the page option or method is used. =head2 offset =over 4 =item Value: $offset =back Specifies the (zero-based) row number for the first row to be returned, or the of the first row of the first page if paging is used. =head2 page NOT IMPLEMENTED. =head2 group_by =over 4 =item Value: \@columns =back A arrayref of columns to group by. Can include columns of joined tables. group_by => [qw/ column1 column2 ... /] =head2 having =over 4 =item Value: \%condition =back The HAVING operator specifies a B<secondary> condition applied to the set after the grouping calculations have been done. In other words it is a constraint just like L</QUERY> (and accepting the same L<SQL::Abstract syntax|SQL::Abstract/WHERE CLAUSES>) applied to the data as it exists after GROUP BY has taken place. Specifying L</having> without L</group_by> is a logical mistake, and a fatal error on most RDBMS engines. Valid fields for criteria are all known attributes, relationships and related attributes for the type this cursor is bound to. E.g. $eav->resultset('CD')->search(undef, { '+select' => { count => 'tracks' }, # alias 'count_tracks' created automatically group_by => ['me.id'], having => { count_tracks => { '>' => 5 } } }); Althought literal SQL is supported, you must know the actual alias and column names used in the generated SQL statement. having => \[ 'count(cds_link.) >= ?', 100 ] Set the debug flag to get the SQL statements printed to stderr. =head2 distinct =over 4 =item Value: (0 | 1) =back Set to 1 to automatically generate a L</group_by> clause based on the selection (including intelligent handling of L</order_by> contents). Note that the group criteria calculation takes place over the B<final> selection. This includes any L</+columns>, L</+select> or L</order_by> additions in subsequent L</search> calls, and standalone columns selected via L<DBIx::Class::ResultSetColumn> (L</get_column>). A notable exception are the extra selections specified via L</prefetch> - such selections are explicitly excluded from group criteria calculations. If the cursor also explicitly has a L</group_by> attribute, this setting is ignored and an appropriate warning is issued. =head2 subtype_depth =over 4 =item Value: $depth Specifies how deep in the type hierarchy you want the query to go. By default its 0, and the query is restricted to the type this cursor is bound to. Even though you can use this option to find entities of subtypes, you cannot use the subtypes own attributes in the query. So if you need to do a subtype query, ensure all attributes needed for the query are defined on the parent type. # Example entity types: # Product [attrs: name, price, description] # HardDrive [extends: Product] [attrs: rpm, capacity] # Monitor [extends: Product] [attrs: resolution, contrast_ratio] # FancyMonitor [extends: Monitor] [attrs: fancy_feature] # this query won't find any HardDrive or Monitor, only Product entities $eav->resultset('Product')->search({ price => { '<' => 500 } }); # this also finds HardDrive and Monitor entities $eav->resultset('Product')->search( { price => { '<' => 500 } }, # subtype's attributes are not allowed { subtype_depth => 1 } ); # this query also finds FancyMonitor $eav->resultset('Product')->search( \%where, { subtype_depth => 2 } ); =back =head2 prefetch NOT IMPLEMENTED. =head1 LICENSE Copyright (C) Carlos Fernando Avila Gratz. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 AUTHOR Carlos Fernando Avila Gratz E<lt>cafe@kreato.com.brE<gt> =cut