Module: Relationships::ClassMethods

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

Instance Method Summary collapse

Instance Method Details

#add_relationship_dependency(relationship_name, clz) ⇒ Object



925
926
927
928
# File 'backend/app/model/mixins/relationships.rb', line 925

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

#apply_relationships(obj, json, opts, new_record = false) ⇒ Object

Create set of relationships for a given update



801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
# File 'backend/app/model/mixins/relationships.rb', line 801

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

#calculate_object_graph(object_graph, opts = {}) ⇒ Object



665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
# File 'backend/app/model/mixins/relationships.rb', line 665

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

#clear_relationshipsObject

Reset relationship definitions for the current class



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

def clear_relationships
  @relationships = {}
end

#create_from_json(json, opts = {}) ⇒ Object



866
867
868
869
870
# File 'backend/app/model/mixins/relationships.rb', line 866

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

#define_relationship(opts) ⇒ Object

Define a new relationship.



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
# File 'backend/app/model/mixins/relationships.rb', line 712

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

    # Give the new relationship class a name to help with debugging
    # Example: Relationships::ResourceSubject
    Relationships.const_set(self.name + opts[:name].to_s.camelize, clz)

  end
end

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

Delete all existing relationships for ‘obj’.



763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
# File 'backend/app/model/mixins/relationships.rb', line 763

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

#dependent_modelsObject



702
703
704
# File 'backend/app/model/mixins/relationships.rb', line 702

def dependent_models
  @relationship_dependencies.values.flatten.uniq
end

#eager_load_relationships(objects, relationships_to_load = nil) ⇒ Object

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.



852
853
854
855
856
857
858
859
860
861
862
863
# File 'backend/app/model/mixins/relationships.rb', line 852

def eager_load_relationships(objects, relationships_to_load = nil)
  relationships_to_load = relationships unless relationships_to_load

  relationships_to_load.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

#find_relationship(name, noerror = false) ⇒ Object



707
708
709
# File 'backend/app/model/mixins/relationships.rb', line 707

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

#instances_relating_to(obj) ⇒ Object

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



918
919
920
921
922
# File 'backend/app/model/mixins/relationships.rb', line 918

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

#relationship_dependenciesObject



697
698
699
# File 'backend/app/model/mixins/relationships.rb', line 697

def relationship_dependencies
  @relationship_dependencies
end

#relationshipsObject



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

def relationships
  @relationships.values
end

#sequel_to_jsonmodel(objs, opts = {}) ⇒ Object



873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
# File 'backend/app/model/mixins/relationships.rb', line 873

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

  return jsons if opts[:skip_relationships]

  eager_load_relationships(objs, relationships.select {|relationship_defn| relationship_defn.json_property})

  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.



939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
# File 'backend/app/model/mixins/relationships.rb', line 939

def touch_mtime_of_anyone_related_to(obj)
  now = Time.now

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

    # If this relationship doesn't link to records of type `obj`, we're not
    # interested.
    next unless 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|

        # This one type of relationship (between the software agent and
        # anything else) was a particular hotspot when analyzing real-world
        # performance.
        #
        # 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.
        #
        if DB.supports_join_updates? &&
           self.table_name == :agent_software &&
           relationship_defn.table_name == :linked_agents_rlshp

          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

          return
        end

        # 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


        # Join our model class table to the relationship that links it to `obj`
        #
        # For example: join ArchivalObject to subject_rlshp
        #              join Instance to instance_do_link_rlshp
        base_ds = self.join(relationship_defn.table_name,
                            Sequel.qualify(relationship_defn.table_name, my_col) =>
                                   Sequel.qualify(self.table_name, :id))

        # Limit only to the object of interest--we only care about records
        # involved in a relationship with the record that was updated (obj)
        base_ds = base_ds.filter(Sequel.qualify(relationship_defn.table_name, their_col) => obj.id)

        # Now update the mtime of any top-level record that links to that
        # relationship.
        self.update_toplevel_mtimes(base_ds, now)
      end
    end
  end
end

#transfer(relationship_name, target, victims) ⇒ Object



931
932
933
934
# File 'backend/app/model/mixins/relationships.rb', line 931

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

#update_toplevel_mtimes(dataset, new_mtime) ⇒ Object

Given a dataset that links the current record type to some relationship type, set the modification time of the nearest top-level record to new_mtime.

If the current record type links directly to the relationship (such as an Archival Object linking to a Subject), then this is easy: we just update the modification time of the Archival Object.

If the current record is a nested record (such as an Instance linked to a Digital Object), we want to continue up the chain, linking the Instance nested record to its Accession/Resource/Archival Object parent record, and then update the modification time of that parent.

And if the nested record has a nested record has a nested record has a relationship… well, you get the idea. We handle the recursive case too!



1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
# File 'backend/app/model/mixins/relationships.rb', line 1025

def update_toplevel_mtimes(dataset, new_mtime)
  if self.enclosing_associations.empty?
    # If we're not enclosed by anything else, we're a top-level record.  Do the final update.
    if DB.supports_join_updates?
      # Fast path!  Use a join update.
      dataset.update(Sequel.qualify(self.table_name, :system_mtime) => new_mtime)
    else
      # Slow path.  Subselect.
      ids_to_touch = dataset.select(Sequel.qualify(self.table_name, :id))
      self.filter(:id => ids_to_touch).update(:system_mtime => new_mtime)
    end
  else
    # Otherwise, we're a nested record
    self.enclosing_associations.each do |association|
      parent_model = association[:model]

      # Link the parent into the current dataset
      parent_ds = dataset.join(parent_model.table_name,
                               Sequel.qualify(self.table_name, association[:key]) =>
                                      Sequel.qualify(parent_model.table_name, :id))

      # and tell it to continue!
      parent_model.update_toplevel_mtimes(parent_ds, new_mtime)
    end
  end
end