Module: Relationships::ClassMethods

Defined in:
backend/app/model/mixins/relationships.rb

Instance Method Summary (collapse)

Instance Method Details

- (Object) add_relationship_dependency(relationship_name, clz)



693
694
695
696
# File 'backend/app/model/mixins/relationships.rb', line 693

def add_relationship_dependency(relationship_name, clz)
  @relationship_dependencies[relationship_name] ||= []
  @relationship_dependencies[relationship_name] << clz
end

- (Object) apply_relationships(obj, json, opts, new_record = false)

Create set of relationships for a given update



571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
# File 'backend/app/model/mixins/relationships.rb', line 571

def apply_relationships(obj, json, opts, new_record = false)
  delete_existing_relationships(obj) if !new_record

  @relationships.each do |relationship_name, relationship_defn|
    property_name = relationship_defn.json_property

    # If there's no property name, the relationship is just read-only
    next if !property_name

    # For each record reference in our JSON data
    ASUtils.as_array(json[property_name]).each_with_index do |reference, idx|
      record_type = parse_reference(reference['ref'], opts)

      referent_model = relationship_defn.participating_models.find {|model|
        model.my_jsonmodel.record_type == record_type[:type]
      } or raise "Couldn't find model for #{record_type[:type]}"

      referent = referent_model[record_type[:id]]

      if !referent
        raise ReferenceError.new("Can't relate to non-existent record: #{reference['ref']}")
      end

      # Create a new relationship instance linking us and them together, and
      # add the properties from the JSON request to the relationship
      properties = reference.clone.tap do |properties|
        properties.delete('ref')
      end

      properties[:aspace_relationship_position] = idx
      properties[:system_mtime] = Time.now
      properties[:user_mtime] = Time.now

      relationship_defn.relate(obj, referent, properties)

      # If this is a reciprocal relationship (defined on both participating
      # models), update the referent's lock version to ensure that a
      # concurrent update to that object won't clobber our changes.

      if referent_model.find_relationship(relationship_name, true) && !opts[:system_generated]
        DB.increase_lock_version_or_fail(referent)
      end
    end
  end
end

- (Object) calculate_object_graph(object_graph, opts = {})



441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'backend/app/model/mixins/relationships.rb', line 441

def calculate_object_graph(object_graph, opts = {})
  # For each relationship involving a resource
  self.relationships.each do |relationship_defn|
    # Find any relationship of this type involving any record mentioned in
    # object graph

    object_graph.each do |model, id_list|
      next unless relationship_defn.participating_models.include?(model)

      linked_relationships = relationship_defn.find_by_participant_ids(model, id_list).map {|row|
        row[:id]
      }

      object_graph.add_objects(relationship_defn, linked_relationships)
    end
  end

  super
end

- (Object) clear_relationships

Reset relationship definitions for the current class



463
464
465
# File 'backend/app/model/mixins/relationships.rb', line 463

def clear_relationships
  @relationships = {}
end

- (Object) create_from_json(json, opts = {})



634
635
636
637
638
# File 'backend/app/model/mixins/relationships.rb', line 634

def create_from_json(json, opts = {})
  obj = super
  apply_relationships(obj, json, opts, true)
  obj
end

- (Object) define_relationship(opts)

Define a new relationship.



488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'backend/app/model/mixins/relationships.rb', line 488

def define_relationship(opts)
  [:name, :contains_references_to_types].each do |p|
    opts[p] or raise "No #{p} given"
  end

  base = self

  ArchivesSpaceService.loaded_hook do
    # We hold off actually setting anything up until all models have been
    # loaded, since our relationships may need to reference a model that
    # hasn't been loaded yet.
    #
    # This is also why the :contains_references_to_types property is a proc
    # instead of a regular array--we don't want to blow up with a NameError
    # if the model hasn't been loaded yet.


    related_models = opts[:contains_references_to_types].call

    clz = Class.new(AbstractRelationship) do
      table = "#{opts[:name]}_rlshp".intern
      set_dataset(table)
      set_primary_key(:id)

      if !self.db.table_exists?(self.table_name)
        Log.warn("Table doesn't exist: #{self.table_name}")
      end

      set_participating_models([base, *related_models].uniq)
      set_json_property(opts[:json_property])
      set_wants_array(opts[:is_array].nil? || opts[:is_array])
    end

    opts[:class_callback].call(clz) if opts[:class_callback]

    @relationships[opts[:name]] = clz

    related_models.each do |model|
      model.include(Relationships)
      model.add_relationship_dependency(opts[:name], base)
    end
  end
end

- (Object) delete_existing_relationships(obj, bump_lock_version_on_referent = false, force = false, predicate = nil)

Delete all existing relationships for ‘obj’.



534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'backend/app/model/mixins/relationships.rb', line 534

def delete_existing_relationships(obj, bump_lock_version_on_referent = false, force = false, predicate = nil)
  relationships.each do |relationship_defn|

    next if (!relationship_defn.json_property && !force)

    if (relationship_defn.json_property &&
        (!self.my_jsonmodel.schema['properties'][relationship_defn.json_property] ||
         self.my_jsonmodel.schema['properties'][relationship_defn.json_property]['readonly'] === 'true'))
      # Don't delete instances of relationships that are read-only in this direction.
      next
    end


    relationship_defn.find_by_participant(obj).each do |relationship|

      # If our predicate says to spare this relationship, leave it alone
      next if predicate && !predicate.call(relationship)

      # If we're deleting a relationship without replacing it, bump the lock
      # version on the referent object so it doesn't accidentally get
      # re-added.
      #
      # This will also encourage the indexer to pick up changes on deletion
      # (e.g. a subject gets deleted and we want to reindex the records that
      # reference it)
      if bump_lock_version_on_referent
        referent = relationship.other_referent_than(obj)
        DB.increase_lock_version_or_fail(referent) if referent
      end

      relationship.delete
    end
  end
end

- (Object) dependent_models



478
479
480
# File 'backend/app/model/mixins/relationships.rb', line 478

def dependent_models
  @relationship_dependencies.values.flatten.uniq
end

- (Object) eager_load_relationships(objects)

Find all of the relationships involving ‘objects’ and tell each object to cache its relationships. This is an optimisation: avoids the need for one SELECT for every relationship lookup by pulling back all relationships at once.



622
623
624
625
626
627
628
629
630
631
# File 'backend/app/model/mixins/relationships.rb', line 622

def eager_load_relationships(objects)
  relationships.each do |relationship_defn|
    # For each defined relationship
    relationships_map = relationship_defn.find_by_participants(objects)

    objects.each do |obj|
      obj.cache_relationships(relationship_defn, relationships_map[obj])
    end
  end
end

- (Object) find_relationship(name, noerror = false)



483
484
485
# File 'backend/app/model/mixins/relationships.rb', line 483

def find_relationship(name, noerror = false)
  @relationships[name] or (noerror ? nil : raise("Couldn't find #{name} in #{@relationships.inspect}"))
end

- (Object) instances_relating_to(obj)

Find all instances of the referring class that have a relationship with ‘obj’ Spans all defined relationships.



686
687
688
689
690
# File 'backend/app/model/mixins/relationships.rb', line 686

def instances_relating_to(obj)
  relationships.map {|relationship_defn|
    relationship_defn.who_participates_with(obj)
  }.flatten
end

- (Object) relationship_dependencies



473
474
475
# File 'backend/app/model/mixins/relationships.rb', line 473

def relationship_dependencies
  @relationship_dependencies
end

- (Object) relationships



468
469
470
# File 'backend/app/model/mixins/relationships.rb', line 468

def relationships
  @relationships.values
end

- (Object) sequel_to_jsonmodel(objs, opts = {})



641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
# File 'backend/app/model/mixins/relationships.rb', line 641

def sequel_to_jsonmodel(objs, opts = {})
  jsons = super

  return jsons if opts[:skip_relationships]

  eager_load_relationships(objs)

  jsons.zip(objs).each do |json, obj|
    relationships.each do |relationship_defn|
      property_name = relationship_defn.json_property

      # If we don't need this property in our return JSON, skip it.
      next unless property_name

      # For each defined relationship
      relationships = if obj.cached_relationships
                        # Use the eagerly fetched relationships if we have them
                        Array(obj.cached_relationships[relationship_defn])
                      else
                        relationship_defn.find_by_participant(obj)
                      end

      json[property_name] = relationships.map {|relationship|
        next if RequestContext.get(:enforce_suppression) && relationship.suppressed == 1

        # Return the relationship properties, plus the URI reference of the
        # related object
        values = ASUtils.keys_as_strings(relationship.properties)
        values['ref'] = relationship.uri_for_other_referent_than(obj)

        values
      }

      if !relationship_defn.wants_array?
        json[property_name] = json[property_name].first
      end
    end
  end

  jsons
end

This notifies the current model that an instance of a related model has been changed. We respond by finding any of our own instances that refer to the updated instance and update their mtime.



707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
# File 'backend/app/model/mixins/relationships.rb', line 707

def touch_mtime_of_anyone_related_to(obj)
  now = Time.now

  relationships.map do |relationship_defn|
    models = relationship_defn.participating_models

    if models.include?(obj.class)
      their_ref_columns = relationship_defn.reference_columns_for(obj.class)
      my_ref_columns = relationship_defn.reference_columns_for(self)
      their_ref_columns.each do |their_col|
        my_ref_columns.each do |my_col|

          # Example: if we're updating a subject record and want to update
          # the timestamps of any linked archival object records:
          #
          #  * self = ArchivalObject
          #  * relationship_defn is subject_rlshp
          #  * obj = #<Subject instance that was updated>
          #  * their_col = subject_rlshp.subject_id
          #  * my_col = subject_rlshp.archival_object_id

          if DB.supports_join_updates?

            if self.table_name == :agent_software && relationship_defn.table_name == :linked_agents_rlshp
              # Terrible to have to do this, but the MySQL optimizer refuses
              # to use the primary key on agent_software because it (often)
              # only has one row.
              DB.open do |db|
                id_str = Integer(obj.id).to_s

                db.run("UPDATE `agent_software` FORCE INDEX (PRIMARY) " +
                       " INNER JOIN `linked_agents_rlshp` " +
                       "ON (`linked_agents_rlshp`.`agent_software_id` = `agent_software`.`id`) " +
                       "SET `agent_software`.`system_mtime` = NOW() " +
                       "WHERE (`linked_agents_rlshp`.`archival_object_id` = #{id_str})")
              end
            else
              # MySQL will optimize this much more aggressively
              self.join(relationship_defn, Sequel.qualify(relationship_defn.table_name, my_col) => Sequel.qualify(self.table_name, :id)).
                filter(Sequel.qualify(relationship_defn.table_name, their_col) => obj.id).
                update(Sequel.qualify(self.table_name, :system_mtime) => now)
            end

          else
            ids_to_touch = relationship_defn.filter(their_col => obj.id).
                           select(my_col)
            self.filter(:id => ids_to_touch).
              update(:system_mtime => now)
          end
        end
      end
    end
  end
end

- (Object) transfer(relationship_name, target, victims)



699
700
701
702
# File 'backend/app/model/mixins/relationships.rb', line 699

def transfer(relationship_name, target, victims)
  relationship = find_relationship(relationship_name)
  relationship.transfer(target, victims)
end