Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 4f7837d7

Von Sven Schöling vor mehr als 10 Jahren hinzugefügt

  • ID 4f7837d76dff9243196ac8fb3c4099215e32c514
  • Vorgänger adb2f4cd
  • Nachfolger df2fba09

SL::DB::Helper::LinkedRecords: rekursive Suche in linked_records

Unterschiede anzeigen:

SL/DB/Helper/LinkedRecords.pm
8 8

  
9 9
use Carp;
10 10
use Sort::Naturally;
11
use SL::DBUtils;
11 12

  
12 13
use SL::DB::Helper::Mappings;
13 14
use SL::DB::RecordLink;
......
88 89
  };
89 90

  
90 91
  # If no 'via' is given then use a simple(r) method for querying the wanted objects.
91
  if (!$params{via}) {
92
  if (!$params{via} && !$params{recursive}) {
92 93
    my @query = ( "${myself}_table" => $my_table,
93 94
                  "${myself}_id"    => $self->id );
94 95
    push @query, ( "${wanted}_table" => $wanted_tables ) if $wanted_tables;
......
97 98
  }
98 99

  
99 100
  # More complex handling for the 'via' case.
100
  my @sources = ( $self );
101
  my @targets = map { SL::DB::Helper::Mappings::get_table_for_package($_) } @{ ref($params{via}) ? $params{via} : [ $params{via} ] };
102
  push @targets, @{ $wanted_tables } if $wanted_tables;
103

  
104
  my %seen = map { ($_->meta->table . $_->id => 1) } @sources;
105

  
106
  while (@targets) {
107
    my @new_sources = @sources;
108
    foreach my $src (@sources) {
109
      my @query = ( "${myself}_table" => $src->meta->table,
110
                    "${myself}_id"    => $src->id,
111
                    "${wanted}_table" => \@targets );
112
      push @new_sources,
113
           map  { $get_objects->($_) }
114
           grep { !$seen{$_->$sub_wanted_table . $_->$sub_wanted_id} }
115
           @{ SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]) };
101
  if ($params{via}) {
102
    my @sources = ( $self );
103
    my @targets = map { SL::DB::Helper::Mappings::get_table_for_package($_) } @{ ref($params{via}) ? $params{via} : [ $params{via} ] };
104
    push @targets, @{ $wanted_tables } if $wanted_tables;
105

  
106
    my %seen = map { ($_->meta->table . $_->id => 1) } @sources;
107

  
108
    while (@targets) {
109
      my @new_sources = @sources;
110
      foreach my $src (@sources) {
111
        my @query = ( "${myself}_table" => $src->meta->table,
112
                      "${myself}_id"    => $src->id,
113
                      "${wanted}_table" => \@targets );
114
        push @new_sources,
115
             map  { $get_objects->($_) }
116
             grep { !$seen{$_->$sub_wanted_table . $_->$sub_wanted_id} }
117
             @{ SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]) };
118
      }
119

  
120
      @sources = @new_sources;
121
      %seen    = map { ($_->meta->table . $_->id => 1) } @sources;
122
      shift @targets;
116 123
    }
117 124

  
118
    @sources = @new_sources;
119
    %seen    = map { ($_->meta->table . $_->id => 1) } @sources;
120
    shift @targets;
125
    my %wanted_tables_map = map  { ($_ => 1) } @{ $wanted_tables };
126
    return [ grep { $wanted_tables_map{$_->meta->table} } @sources ];
121 127
  }
122 128

  
123
  my %wanted_tables_map = map  { ($_ => 1) } @{ $wanted_tables };
124
  return [ grep { $wanted_tables_map{$_->meta->table} } @sources ];
129
  # And lastly recursive mode
130
  if ($params{recursive}) {
131
    # don't use rose retrieval here. too slow.
132
    # instead use recursive sql to get all the linked record_links entrys, and retrieve the objects from there
133
    my $query = <<"";
134
      WITH RECURSIVE record_links_rec_${wanted}(id, from_table, from_id, to_table, to_id, depth, path, cycle) AS (
135
        SELECT id, from_table, from_id, to_table, to_id,
136
          1, ARRAY[id], false
137
        FROM record_links
138
        WHERE ${myself}_id = ? and ${myself}_table = ?
139
      UNION ALL
140
        SELECT rl.id, rl.from_table, rl.from_id, rl.to_table, rl.to_id,
141
          rlr.depth + 1, path || rl.id, rl.id = ANY(path)
142
        FROM record_links rl, record_links_rec_${wanted} rlr
143
        WHERE rlr.${wanted}_id = rl.${myself}_id AND rlr.${wanted}_table = rl.${myself}_table AND NOT cycle
144
      )
145
      SELECT DISTINCT ON (${wanted}_table, ${wanted}_id)
146
        id, from_table, from_id, to_table, to_id, path, depth FROM record_links_rec_${wanted}
147
      WHERE NOT cycle
148
      ORDER BY ${wanted}_table, ${wanted}_id, depth ASC;
149

  
150
    my $links     = selectall_hashref_query($::form, $::form->get_standard_dbh, $query, $self->id, $self->meta->table);
151
    my $link_objs = SL::DB::Manager::RecordLink->get_all(query => [ id => [ map { $_->{id} } @$links ] ]);
152
    my @objects = map { $get_objects->($_) } @$link_objs;
153

  
154
    if ($params{save_path}) {
155
       my %links_by_id = map { $_->{id} => $_ } @$links;
156
       for (@objects) {
157
         $_->{_record_link_path}  = $links_by_id{$_->{_record_link}->id}->{path};
158
         $_->{_record_link_depth} = $links_by_id{$_->{_record_link}->id}->{depth};
159
       }
160
    }
161

  
162
    return \@objects;
163
  }
125 164
}
126 165

  
127 166
sub link_to_record {
......
276 315
    from      => 'Order',
277 316
  );
278 317

  
279
  # transitive over known classes
318
  # via over known classes
280 319
  my @linked_objects = $order->linked_records(
281
    direction => 'to',
282 320
    to        => 'Invoice',
283 321
    via       => 'DeliveryOrder',
284 322
  );
323
  my @linked_objects = $order->linked_records(
324
    to        => 'Invoice',
325
    via       => [ 'Order', 'DeliveryOrder' ],
326
  );
327

  
328
  # recursive
329
  my @linked_objects = $order->linked_records(
330
    recursive => 1,
331
  );
332

  
285 333

  
286 334
  # limit direction when further params contain additional keys
287 335
  my %params = (to => 'Invoice', from => 'Order');
......
356 404
    query     => [ transdate => DateTime->today_local ],
357 405
  );
358 406

  
407
In case you don't know or care which or how many objects are visited the flag
408
C<recursive> can be used. It searches all reachable objects in the given direction:
409

  
410
  my $records = $order->linked_records(
411
    direction => 'to',
412
    recursive => 1,
413
  );
414

  
415
Only link chains of the same type will be considered. So even with direction
416
both, this
417

  
418
  order 1 ---> invoice <--- order 2
419

  
420
started from order 1 will only find invoice. If an object is found both in each
421
direction, only one copy will be returned. The recursion is cycle protected,
422
and will not recurse infinitely. Cycles are defined by the same link being
423
visited twice, so this
424

  
425

  
426
  order 1 ---> order 2 <--> delivery order
427
                 |
428
                 `--------> invoice
429

  
430
will find the path o1 -> o2 -> do -> o2 -> i without considering it a cycle.
431

  
432
The optional extra flag C<save_path> will give you extra inforamtion saved in
433
the returned objects:
434

  
435
  my $records = $order->linked_records(
436
    direction => 'to',
437
    recursive => 1,
438
    save_path => 1,
439
  );
440

  
441
Every record will have two fields set:
442

  
443
=over 2
444

  
445
=item C<_record_link_path>
446

  
447
And array with the ids of the visited links. The shortest paths will be
448
prefered, so in the previous example this would contain the ids of o1-o2 and
449
o2-i.
450

  
451
=item C<_record_link_depth>
452

  
453
Recursion depth when this object was found. Equal to the number of ids in
454
C<_record_link_path>
455

  
456
=back
457

  
458

  
359 459
The optional parameters C<$params{sort_by}> and C<$params{sort_dir}>
360 460
can be used in order to sort the result. If C<$params{sort_by}> is
361 461
trueish then the result is sorted by calling L</sort_linked_records>.
......
450 550

  
451 551
Nothing here yet.
452 552

  
553
=head1 TODO
554

  
555
 * C<recursive> should take a query param depth and cut off there
556
 * C<recursive> uses partial distinct which is known to be not terribly fast on
557
   a million entry table. replace with a better statement if this ever becomes
558
   an issue.
559

  
453 560
=head1 AUTHOR
454 561

  
455 562
Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
563
Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
456 564

  
457 565
=cut
t/db_helper/record_links.t
1
use Test::More tests => 43;
1
use Test::More tests => 49;
2 2

  
3 3
use strict;
4 4

  
......
9 9
use Data::Dumper;
10 10
use Support::TestSetup;
11 11
use Test::Exception;
12
use List::Util qw(max);
12 13

  
13 14
use SL::DB::Buchungsgruppe;
14 15
use SL::DB::Currency;
......
167 168

  
168 169
$o2->link_to_record($d);
169 170
$d->link_to_record($i);
170

  
171
# at this point the structure is:
172
#
173
#   o1 <--> o2 ---> d ---> i
174
#
171 175

  
172 176
$links = $d->linked_records(direction => 'both', to => 'Invoice', from => 'Order', sort_by => 'customer_id', sort_dir => 1);
173 177
is $links->[0]->id, $o2->id, 'both with different from/to 1';
......
181 185
is @$links, 1, 'double link is only added once 1';
182 186

  
183 187
$d->link_to_record($o2, bidirectional => 1);
188
# at this point the structure is:
189
#
190
#   o1 <--> o2 <--> d ---> i
191
#
184 192

  
185 193
$links = $o2->linked_records(direction => 'to', to => 'DeliveryOrder');
186 194
is @$links, 1, 'double link is only added once 2';
......
203 211
is $links->[0]->{_record_link_direction}, 'from',  '_record_link_direction from';
204 212
is $links->[0]->{_record_link}->to_id, $o1->id,  '_record_link from';
205 213

  
206
# check if bidi returns an array of links
214
# check if bidi returns an array of links even if aready existing
207 215
my @links = $d->link_to_record($o2, bidirectional => 1);
216
# at this point the structure is:
217
#
218
#   o1 <--> o2 <--> d ---> i
219
#
208 220
is @links, 2, 'bidi returns array of links in array context';
209 221

  
210 222
#  via
......
219 231

  
220 232
# multiple links in the same direction from one object
221 233
$o1->link_to_record($d);
222
$links = $o2->linked_records(direction => 'to', to => 'Invoice', via => 'DeliveryOrder');
223
is $links->[0]->id, $i->id,  'simple case via links (string)';
224

  
225 234
# at this point the structure is:
226 235
#
227
#   o1 <--> o2 ---> d ---> i
236
#   o1 <--> o2 <--> d ---> i
228 237
#     \____________,^
229 238
#
230 239

  
240
$links = $o2->linked_records(direction => 'to', to => 'Invoice', via => 'DeliveryOrder');
241
is $links->[0]->id, $i->id,  'simple case via links (string)';
242

  
243

  
231 244
# o1 must have 2 linked records now:
232 245
$links = $o1->linked_records(direction => 'to');
233 246
is @$links, 2,  'more than one link';
......
278 291
$sorted = SL::DB::Helper::LinkedRecords->sort_linked_records('date', 0, @records);
279 292
is_deeply $sorted, [$d, $o1, $i, $o2], 'sorting by transdate desc';
280 293

  
294
# now recursive stuff 2, with backlinks
295
$links = $o1->linked_records(direction => 'to', recursive => 1, save_path => 1);
296
is @$links, 4, 'recursive finds all 4 (backlink to self because of bidi o1<->o2)';
297

  
298
# because of the link o1->d the longest path should be legth 2. test that
299
is max(map { $_->{_record_link_depth} } @$links), 2, 'longest path is 2';
300

  
301
$links = $o2->linked_records(direction => 'to', recursive => 1);
302
is @$links, 4, 'recursive from o2 finds 4';
303

  
304
$links = $o1->linked_records(direction => 'from', recursive => 1, save_path => 1);
305
is @$links, 3, 'recursive from o1 finds 3 (not i)';
306

  
307
$links = $i->linked_records(direction => 'from', recursive => 1, save_path => 1);
308
is @$links, 3, 'recursive from i finds 3 (not i)';
309

  
310
$links = $o1->linked_records(direction => 'both', recursive => 1, save_path => 1);
311
is @$links, 4, 'recursive dir=both does not give duplicates';
281 312
1;

Auch abrufbar als: Unified diff