]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI.pm
null cleared fields
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Storage / CDBI.pm
1 package OpenILS::Application::Storage::CDBI;
2 use base qw/Class::DBI/;
3 use Class::DBI;
4 use Class::DBI::AbstractSearch;
5
6 use OpenILS::Application::Storage::CDBI::actor;
7 use OpenILS::Application::Storage::CDBI::action;
8 use OpenILS::Application::Storage::CDBI::asset;
9 use OpenILS::Application::Storage::CDBI::authority;
10 use OpenILS::Application::Storage::CDBI::biblio;
11 use OpenILS::Application::Storage::CDBI::config;
12 use OpenILS::Application::Storage::CDBI::metabib;
13 use OpenILS::Application::Storage::CDBI::money;
14 use OpenILS::Application::Storage::CDBI::permission;
15 use OpenILS::Application::Storage::CDBI::container;
16
17 use JSON;
18 use OpenSRF::Utils::Logger qw(:level);
19 use OpenSRF::EX qw/:try/;
20
21 our $VERSION = 1;
22 my $log = 'OpenSRF::Utils::Logger';
23
24 sub child_init {
25         my $self = shift;
26
27         $log->debug("Creating ImaDBI Querys", DEBUG);
28         __PACKAGE__->set_sql( 'OILSFastSearch', <<"     SQL", 'Main');
29                 SELECT  %s
30                   FROM  %s
31                   WHERE %s = ?
32         SQL
33
34         __PACKAGE__->set_sql( 'OILSFastOrderedSearchLike', <<"  SQL", 'Main');
35                 SELECT  %s
36                   FROM  %s
37                   WHERE %s LIKE ?
38                   ORDER BY %s
39         SQL
40
41         __PACKAGE__->set_sql( 'OILSFastOrderedSearch', <<"      SQL", 'Main');
42                 SELECT  %s
43                   FROM  %s
44                   WHERE %s = ?
45                   ORDER BY %s
46         SQL
47
48         $log->debug("Calling Driver child_init", DEBUG);
49         $self->SUPER::child_init(@_);
50
51 }
52
53 sub fast_flesh_sth {
54         my $class = shift;
55         $class = ref($class) || $class;
56
57         my $field = shift;
58         my $value = shift;
59         my $order = shift;
60         my $like = shift;
61
62
63         if (!(defined($order) and ref($order) and ref($order) eq 'HASH')) {
64                 if (defined($value) and ref($value) and ref($value) eq 'HASH') {
65                         $order = $value;
66                         $value = undef;
67                 } else {
68                         $order = { order_by => $class->columns('Primary') }
69                 }
70         }
71
72         unless (defined $value) {
73                 $value = $field;
74                 ($field) = $class->columns('Primary');
75         }
76
77         unless (defined $field) {
78                 ($field) = $class->columns('Primary');
79         }
80
81         unless ($order->{order_by}) {
82                 $order = { order_by => $class->columns('Primary') }
83         }
84
85         my $fm_class = 'Fieldmapper::'.$class;
86         my $field_list = join ',', $class->columns('Essential');
87         
88         my $sth;
89         if (!$like) {
90                 $sth = $class->sql_OILSFastOrderedSearch( $field_list, $class->table, $field, $order->{order_by});
91         } else {
92                 $sth = $class->sql_OILSFastOrderedSearchLike( $field_list, $class->table, $field, $order->{order_by});
93         }
94         $sth->execute($value);
95         return $sth;
96 }
97
98 sub fast_flesh {
99         my $self = shift;
100         return map $class->construct($_), $self->fast_flesh_sth(@_)->fetchall_hash;
101 }
102
103 sub fast_fieldmapper {
104         my $self = shift;
105         my $id = shift;
106         my $col = shift;
107         my $like = shift;
108         my $options = shift;
109         my $class = ref($self) || $self;
110         my $fm_class = 'Fieldmapper::'.$class;
111         my @fms;
112         $log->debug("fast_fieldmapper() ==> Retrieving $fm_class", INTERNAL);
113         if ($like < 2) {
114                 for my $hash ($self->fast_flesh_sth( $col, "$id", { order_by => $col }, $like )->fetchall_hash) {
115                         my $fm = $fm_class->new;
116                         for my $field ( $fm_class->real_fields ) {
117                                 $fm->$field( $$hash{$field} );
118                         }
119                         push @fms, $fm;
120                 }
121         } else {
122                 my $search_type = 'search';
123                 if ($like == 2) {
124                         $search_type = 'search_fts'
125                 } elsif ($like == 3) {
126                         $search_type = 'search_regex'
127                 }
128
129                 for my $obj ($class->$search_type({ $col => $id}, $options)) {
130                         push @fms, $obj->to_fieldmapper;
131                 }
132         }
133         return @fms;
134 }
135
136 sub retrieve {
137         my $self = shift;
138         my $arg = shift;
139         if (ref($arg) &&
140                 (UNIVERSAL::isa($arg => 'Fieldmapper') ||
141                  UNIVERSAL::isa($arg => 'Class::DBI')) ) {
142                 my ($col) = $self->primary_column;
143                 $log->debug("Using field $col as the primary key", INTERNAL);
144                 $arg = $arg->$col;
145         } elsif (ref $arg) {
146                 my ($col) = $self->primary_column;
147                 $log->debug("Using field $col as the primary key", INTERNAL);
148                 $arg = $arg->{$col};
149         }
150                 
151         $log->debug("Retrieving $self with $arg", INTERNAL);
152         my $rec;
153         try {
154                 $rec = $self->SUPER::retrieve("$arg");
155         } catch Error with {
156                 $log->debug("Could not retrieve $self with $arg! -- ".shift(), DEBUG);
157                 return undef;
158         };
159         return $rec;
160 }
161
162 sub to_fieldmapper {
163         my $obj = shift;
164         my $class = ref($obj) || $obj;
165
166         my $fm_class = 'Fieldmapper::'.$class;
167         my $fm = $fm_class->new;
168
169         if (ref($obj)) {
170                 for my $field ( $fm->real_fields ) {
171                         $fm->$field( $obj->$field );
172                 }
173         }
174
175         return $fm;
176 }
177
178 sub merge {
179         my $self = shift;
180         my $search = shift;
181         my $arg = shift;
182
183         delete $$arg{$_} for (keys %$search);
184
185         $log->debug("CDBI->merge: \$search is $search (".ref($search)." : ".join(',',map{"$_ => $$search{$_}"}keys(%$search)).")",DEBUG);
186         $log->debug("CDBI->merge: \$arg is $arg (".ref($arg)." : ".join(',',map{"$_ => $$arg{$_}"}keys(%$arg)).")",DEBUG);
187
188         my @objs = ($self);
189         @objs = $self->search_where($search) unless (ref $self);
190
191         if (@objs == 1) {
192                 $objs[0]->update($arg);
193                 return $objs[0];
194         } elsif (@objs == 0) {
195                 return $self->create({%$search,%$arg});
196         } else {
197                 throw OpenSRF::EX::WARN ("Non-unique search key for merge.  Perhaps you meant to use remote_update?");
198         }
199 }
200
201 sub remote_update {
202         my $self = shift;
203         my $search = shift;
204         my $arg = shift;
205
206         delete $$arg{$_} for (keys %$search);
207
208         $log->debug("CDBI->remote_update: \$search is $search (".ref($search)." : ".join(',',map{"$_ => $$search{$_}"}keys(%$search)).")",DEBUG);
209         $log->debug("CDBI->remote_update: \$arg is $arg (".ref($arg)." : ".join(',',map{"$_ => $$arg{$_}"}keys(%$arg)).")",DEBUG);
210
211 #       my @objs = $self->search_where($search);
212 #       throw OpenSRF::EX::WARN ("No objects found for remote_update.  Perhaps you meant to use merge?")
213 #               if (@objs == 0);
214
215 #       $_->update($arg) for (@objs);
216 #       return scalar(@objs);
217
218         my @finds = sort keys %$search;
219         my @sets = sort keys %$arg;
220
221         my @find_vals = @$search{@finds};
222         my @set_vals = @$arg{@sets};
223
224         my $sql = 'UPDATE %s SET %s WHERE %s';
225
226         my $table = $self->table;
227         my $set = join(', ', map { "$_=?" } @sets);
228         my $where = join(', ', map { "$_=?" } @finds);
229
230         my $sth = $self->db_Main->prepare(sprintf($sql, $table, $set, $where));
231         $sth->execute(@set_vals,@find_vals);
232         return $sth->rows;
233
234 }
235
236 sub create {
237         my $self = shift;
238         my $arg = shift;
239
240         $log->debug("CDBI->create: \$arg is $arg (".ref($arg)." : ".JSON->perl2JSON($arg).")",DEBUG);
241
242         if (ref($arg) && UNIVERSAL::isa($arg => 'Fieldmapper')) {
243                 return $self->create_from_fieldmapper($arg,@_);
244         }
245
246         return $self->SUPER::create($arg,@_);
247 }
248
249 sub create_from_fieldmapper {
250         my $obj = shift;
251         my $fm = shift;
252         my @params = @_;
253
254         $log->debug("Creating node of type ".ref($fm), DEBUG);
255
256         my $class = ref($obj) || $obj;
257         my ($primary) = $class->columns('Primary');
258
259         if (ref($fm) &&UNIVERSAL::isa($fm => 'Fieldmapper')) {
260                 my %hash = map { defined $fm->$_ ?
261                                         ($_ => $fm->$_) :
262                                         ()
263                                 } grep { $_ ne $primary } $class->columns('Essential');
264
265                 if ($class->find_column( 'last_xact_id' )) {
266                         if ($OpenILS::Application::Storage::IGNORE_XACT_ID_FAILURE) {
267                                 $hash{last_xact_id} = 'unknown.'.time.'.'.$$.'.'.rand($$);
268                         } else {
269                                 my $xact_id = $class->current_xact_id;
270                                 throw Error unless ($xact_id);
271                                 $hash{last_xact_id} = $xact_id;
272                         }
273                 }
274
275                 return $class->create( \%hash, @params );
276         } else {
277                 return undef;
278         }
279 }
280
281 sub delete {
282         my $self = shift;
283         my $arg = shift;
284         my $orig = $self;
285
286         my $class = ref($self) || $self;
287
288         $self = $self->retrieve($arg) if (!ref($self));
289         unless (defined $self) {
290                 $log->debug("ARG! Couldn't retrieve record ".$arg->id, DEBUG);
291                 throw OpenSRF::EX::WARN ("ARG! Couldn't retrieve record ");
292         }
293
294         if ($class->find_column( 'last_xact_id' )) {
295                 my $xact_id = $self->current_xact_id;
296                 
297                 throw Error ("Deleting from $class requires a transaction be established")
298                         unless ($xact_id);
299                 
300                 throw Error ("The row you are attempting to delete has been changed since you read it")
301                         unless ( $orig->last_xact_id eq $self->last_xact_id);
302
303                 $self->last_xact_id( $class->current_xact_id );
304                 $self->SUPER::update;
305         }
306
307         $self->SUPER::delete;
308
309         return 1;
310 }
311
312 sub debug_object {
313         my $obj = shift;
314         my $string = '';
315
316         $string .= "Object type:\t".ref($obj)."\n";
317         $string .= "Object string:\t$obj\n";
318
319         if (ref($obj) && UNIVERSAL::isa($obj => 'Fieldmapper')) {
320                 $string .= "Object fields:\n";
321                 for my $col ($obj->real_fields()) {
322                         $string .= "\t$col\t=> ".$obj->$col."\n";
323                 }
324         } elsif (ref($obj) && UNIVERSAL::isa($obj => 'Class::DBI')) {
325                 $string .= "Object cols:\n";
326                 for my $col ($obj->columns('All')) {
327                         $string .= "\t$col\t=> ".$obj->$col."\n";
328                 }
329         } elsif (ref($obj) && UNIVERSAL::isa($obj => 'HASH')) {
330                 $string .= "Object keys and vals:\n";
331                 for my $col (keys %$obj) {
332                         $string .= "\t$col\t=> $$obj{$col}\n";
333                 }
334         }
335
336         $string .= "\n";
337         
338         $log->debug($string,DEBUG);
339 }
340
341
342 sub update {
343         my $self = shift;
344         my $arg = shift;
345
346         $log->debug("Attempting to update using $arg", DEBUG) if ($arg);
347
348         if (ref($arg)) {
349                 $self = $self->modify_from_fieldmapper($arg);
350                 unless (defined $self) {
351                         $log->debug("Modification of $arg seems to have failed....", DEBUG);
352                         return undef;
353                 }
354         }
355
356         $log->debug("Calling Class::DBI->update on modified object $self", DEBUG);
357
358         #debug_object($self);
359
360         return $self->SUPER::update if ($self->is_changed);
361         return 0;
362 }
363
364 sub modify_from_fieldmapper {
365         my $obj = shift;
366         my $fm = shift;
367         my $orig = $obj;
368
369         #debug_object($obj);
370         #debug_object($fm);
371
372         $log->debug("Modifying object using fieldmapper", DEBUG);
373
374         my $class = ref($obj) || $obj;
375         my ($primary) = $class->columns('Primary');
376
377
378         if (!ref($obj)) {
379                 $obj = $class->retrieve($fm);
380                 #debug_object($obj);
381                 unless ($obj) {
382                         $log->debug("Retrieve of $class using $fm (".$fm->id.") failed! -- ".shift(), ERROR);
383                         throw OpenSRF::EX::WARN ("No $class with id of ".$fm->id."!!");
384                 }
385         }
386
387         my %hash;
388         
389         if (ref($fm) and UNIVERSAL::isa($fm => 'Fieldmapper')) {
390                 %hash = map { ($_ => ''.$fm->$_) } grep { $_ ne $primary } $class->columns('Essential');
391         } else {
392                 %hash = %{$fm};
393         }
394
395         my $au = $obj->autoupdate;
396         $obj->autoupdate(0);
397         
398         #debug_object($obj);
399
400         for my $field ( keys %hash ) {
401                 $obj->$field( $hash{$field} ) if ($obj->$field ne $hash{$field});
402                 $log->debug("Setting field $field on $obj to $hash{$field}",INTERNAL);
403         }
404
405         if ($class->find_column( 'last_xact_id' ) and $obj->is_changed) {
406                 my ($xact_id) = OpenILS::Application::Storage->method_lookup('open-ils.storage.transaction.current')->run();
407                 throw Error ("Updating $class requires a transaction be established")
408                         unless ($xact_id);
409                 throw Error ("The row you are attempting to delete has been changed since you read it")
410                         unless ( $fm->last_xact_id eq $obj->last_xact_id);
411                 $obj->last_xact_id( $xact_id );
412         } else {
413                 $obj->autoupdate($au)
414         }
415
416         return $obj;
417 }
418
419
420
421         #-------------------------------------------------------------------------------
422         actor::user->has_a( home_ou => 'actor::org_unit' );
423         actor::user->has_a( card => 'actor::card' );
424         actor::user->has_a( standing => 'config::standing' );
425         actor::user->has_a( profile => 'actor::profile' );
426         actor::user->has_a( mailing_address => 'actor::user_address' );
427         actor::user->has_a( billing_address => 'actor::user_address' );
428         actor::user->has_a( ident_type => 'config::identification_type' );
429         actor::user->has_a( ident_type2 => 'config::identification_type' );
430         actor::user->has_a( net_access_level => 'config::net_access_level' );
431
432         actor::user_address->has_a( usr => 'actor::user' );
433         
434         actor::card->has_a( usr => 'actor::user' );
435         
436         actor::org_unit->has_a( parent_ou => 'actor::org_unit' );
437         actor::org_unit->has_a( ou_type => 'actor::org_unit_type' );
438         #actor::org_unit->has_a( address => 'actor::org_address' );
439
440         actor::stat_cat_entry->has_a( stat_cat => 'actor::stat_cat' );
441         actor::stat_cat->has_many( entries => 'actor::stat_cat_entry' );
442         actor::stat_cat_entry_user_map->has_a( stat_cat => 'actor::stat_cat' );
443         actor::stat_cat_entry_user_map->has_a( stat_cat_entry => 'actor::stat_cat_entry' );
444         actor::stat_cat_entry_user_map->has_a( target_usr => 'actor::user' );
445
446         asset::stat_cat_entry->has_a( stat_cat => 'asset::stat_cat' );
447         asset::stat_cat->has_many( entries => 'asset::stat_cat_entry' );
448         asset::stat_cat_entry_copy_map->has_a( stat_cat => 'asset::stat_cat' );
449         asset::stat_cat_entry_copy_map->has_a( stat_cat_entry => 'asset::stat_cat_entry' );
450         asset::stat_cat_entry_copy_map->has_a( owning_copy => 'asset::copy' );
451
452         action::survey_response->has_a( usr => 'actor::user' );
453         action::survey_response->has_a( survey => 'action::survey' );
454         action::survey_response->has_a( question => 'action::survey_question' );
455         action::survey_response->has_a( answer => 'action::survey_answer' );
456
457         action::survey_question->has_a( survey => 'action::survey' );
458
459         action::survey_answer->has_a( question => 'action::survey' );
460
461         asset::copy_note->has_a( owning_copy => 'asset::copy' );
462
463         actor::user->has_many( stat_cat_entries => [ 'actor::stat_cat_entry_user_map' => 'stat_cat_entry' ] );
464         actor::user->has_many( stat_cat_entry_user_maps => 'actor::stat_cat_entry_user_map' );
465
466         asset::copy->has_many( stat_cat_entries => [ 'asset::stat_cat_entry_copy_map' => 'stat_cat_entry' ] );
467         asset::copy->has_many( stat_cat_entry_copy_maps => 'asset::stat_cat_entry_copy_map' );
468
469         asset::copy->has_a( call_number => 'asset::call_number' );
470         asset::copy->has_a( creator => 'actor::user' );
471         asset::copy->has_a( editor => 'actor::user' );
472         asset::copy->has_a( status => 'config::copy_status' );
473         asset::copy->has_a( location => 'asset::copy_location' );
474         asset::copy->has_a( circ_lib => 'actor::org_unit' );
475
476         asset::call_number_note->has_a( call_number => 'asset::call_number' );
477
478         asset::call_number->has_a( record => 'biblio::record_entry' );
479         asset::call_number->has_a( creator => 'actor::user' );
480         asset::call_number->has_a( editor => 'actor::user' );
481
482         authority::record_note->has_a( record => 'authority::record_entry' );
483         biblio::record_note->has_a( record => 'biblio::record_entry' );
484         
485         authority::record_entry->has_a( creator => 'actor::user' );
486         authority::record_entry->has_a( editor => 'actor::user' );
487         biblio::record_entry->has_a( creator => 'actor::user' );
488         biblio::record_entry->has_a( editor => 'actor::user' );
489         
490         metabib::metarecord->has_a( master_record => 'biblio::record_entry' );
491         
492         authority::record_descriptor->has_a( record => 'authority::record_entry' );
493         metabib::record_descriptor->has_a( record => 'biblio::record_entry' );
494         
495         authority::full_rec->has_a( record => 'authority::record_entry' );
496         metabib::full_rec->has_a( record => 'biblio::record_entry' );
497         
498         metabib::title_field_entry->has_a( source => 'biblio::record_entry' );
499         metabib::title_field_entry->has_a( field => 'config::metabib_field' );
500         
501         metabib::author_field_entry->has_a( source => 'biblio::record_entry' );
502         metabib::author_field_entry->has_a( field => 'config::metabib_field' );
503         
504         metabib::subject_field_entry->has_a( source => 'biblio::record_entry' );
505         metabib::subject_field_entry->has_a( field => 'config::metabib_field' );
506         
507         metabib::keyword_field_entry->has_a( source => 'biblio::record_entry' );
508         metabib::keyword_field_entry->has_a( field => 'config::metabib_field' );
509         
510         metabib::series_field_entry->has_a( source => 'biblio::record_entry' );
511         metabib::series_field_entry->has_a( field => 'config::metabib_field' );
512         
513         metabib::metarecord_source_map->has_a( metarecord => 'metabib::metarecord' );
514         metabib::metarecord_source_map->has_a( source => 'biblio::record_entry' );
515
516         action::circulation->has_a( usr => 'actor::user' );
517         action::circulation->has_a( target_copy => 'asset::copy' );
518         action::circulation->has_a( circ_lib => 'actor::org_unit' );
519
520         money::billable_transaction->has_a( usr => 'actor::user' );
521         
522         
523         #-------------------------------------------------------------------------------
524         actor::user->has_many( survey_responses => 'action::survey_response' );
525         actor::user->has_many( addresses => 'actor::user_address' );
526         actor::user->has_many( cards => 'actor::card' );
527
528         actor::org_unit->has_many( users => 'actor::user' );
529         actor::profile->has_many( users => 'actor::user' );
530
531         action::survey->has_many( questions => 'action::survey_question' );
532         action::survey->has_many( responses => 'action::survey_response' );
533         
534         action::survey_question->has_many( answers => 'action::survey_answer' );
535         action::survey_question->has_many( responses => 'action::survey_response' );
536
537         action::survey_answer->has_many( responses => 'action::survey_response' );
538
539         asset::copy->has_many( notes => 'asset::copy_note' );
540         asset::call_number->has_many( copies => 'asset::copy' );
541         asset::call_number->has_many( notes => 'asset::call_number_note' );
542
543         authority::record_entry->has_many( record_descriptor => 'authority::record_descriptor' );
544         authority::record_entry->has_many( notes => 'authority::record_note' );
545
546         biblio::record_entry->has_many( record_descriptor => 'metabib::record_descriptor' );
547         biblio::record_entry->has_many( notes => 'biblio::record_note' );
548         biblio::record_entry->has_many( call_numbers => 'asset::call_number' );
549         biblio::record_entry->has_many( full_record_entries => 'metabib::full_rec' );
550         biblio::record_entry->has_many( title_field_entries => 'metabib::title_field_entry' );
551         biblio::record_entry->has_many( author_field_entries => 'metabib::author_field_entry' );
552         biblio::record_entry->has_many( subject_field_entries => 'metabib::subject_field_entry' );
553         biblio::record_entry->has_many( keyword_field_entries => 'metabib::keyword_field_entry' );
554         biblio::record_entry->has_many( series_field_entries => 'metabib::series_field_entry' );
555
556         metabib::metarecord->has_many( source_records => [ 'metabib::metarecord_source_map' => 'source'] );
557
558         money::billable_transaction->has_many( billings => 'money::billing' );
559         money::billable_transaction->has_many( payments => 'money::payment' );
560
561         money::grocery->has_many( billings => 'money::billing' );
562         money::grocery->has_many( payments => 'money::payment' );
563
564         money::billing->has_a( xact => 'money::billable_transaction' );
565         money::payment->has_a( xact => 'money::billable_transaction' );
566
567         money::cash_payment->has_a( xact => 'money::billable_transaction' );
568         money::cash_payment->has_a( accepting_usr => 'actor::user' );
569
570         money::check_payment->has_a( xact => 'money::billable_transaction' );
571         money::check_payment->has_a( accepting_usr => 'actor::user' );
572
573         money::credit_card_payment->has_a( xact => 'money::billable_transaction' );
574         money::credit_card_payment->has_a( accepting_usr => 'actor::user' );
575
576         money::forgive_payment->has_a( xact => 'money::billable_transaction' );
577         money::forgive_payment->has_a( accepting_usr => 'actor::user' );
578
579         money::work_payment->has_a( xact => 'money::billable_transaction' );
580         money::work_payment->has_a( accepting_usr => 'actor::user' );
581
582         money::credit_payment->has_a( xact => 'money::billable_transaction' );
583         money::credit_payment->has_a( accepting_usr => 'actor::user' );
584
585         permission::grp_tree->has_a( parent => 'permission::grp_tree' );
586
587         permission::grp_perm_map->has_a( grp => 'permission::grp_tree' );
588         permission::grp_perm_map->has_a(  perm => 'permission::perm_list' );
589         permission::grp_perm_map->has_a(  depth => 'actor::org_unit_type' );
590         
591         permission::usr_perm_map->has_a( usr => 'actor::user' );
592         permission::usr_perm_map->has_a(  perm => 'permission::perm_list' );
593         permission::usr_perm_map->has_a(  depth => 'actor::org_unit_type' );
594         
595         permission::usr_grp_map->has_a(  usr => 'actor::user' );
596         permission::usr_grp_map->has_a(  grp => 'permission::grp_tree' );
597
598         action::hold_notification->has_a(  hold => 'action::hold_request' );
599         
600         action::hold_copy_map->has_a(  hold => 'action::hold_request' );
601         action::hold_copy_map->has_a(  target_copy => 'asset::copy' );
602
603         action::hold_request->has_a(  current_copy => 'asset::copy' );
604         action::hold_request->has_a(  requestor => 'actor::user' );
605         action::hold_request->has_a(  usr => 'actor::user' );
606         action::hold_request->has_a(  pickup_lib => 'actor::org_unit' );
607         action::hold_request->has_a(  request_lib => 'actor::org_unit' );
608
609         action::hold_request->has_many(  notifications => 'action::hold_notification' );
610         action::hold_request->has_many(  eligible_copies => [ 'action::hold_copy_map' => 'target_copy' ] );
611
612         asset::copy->has_many(  holds => [ 'action::hold_copy_map' => 'hold' ] );
613
614 1;