Module: Relationships::ClassMethods
- Defined in:
- backend/app/model/mixins/relationships.rb
Instance Method Summary collapse
-
#add_relationship_dependency(relationship_name, clz) ⇒ Object
-
#apply_relationships(obj, json, opts, new_record = false) ⇒ Object
Create set of relationships for a given update.
-
#calculate_object_graph(object_graph, opts = {}) ⇒ Object
-
#clear_relationships ⇒ Object
Reset relationship definitions for the current class.
-
#create_from_json(json, opts = {}) ⇒ Object
-
#define_relationship(opts) ⇒ Object
Define a new relationship.
-
#delete_existing_relationships(obj, bump_lock_version_on_referent = false, force = false, predicate = nil) ⇒ Object
Delete all existing relationships for ‘obj’.
-
#dependent_models ⇒ Object
-
#eager_load_relationships(objects, relationships_to_load = nil) ⇒ Object
Find all of the relationships involving ‘objects’ and tell each object to cache its relationships.
-
#find_relationship(name, noerror = false) ⇒ Object
-
#instances_relating_to(obj) ⇒ Object
Find all instances of the referring class that have a relationship with ‘obj’ Spans all defined relationships.
-
#relationship_dependencies ⇒ Object
-
#relationships ⇒ Object
-
#sequel_to_jsonmodel(objs, opts = {}) ⇒ Object
-
#touch_mtime_of_anyone_related_to(obj) ⇒ Object
This notifies the current model that an instance of a related model has been changed.
-
#transfer(relationship_name, target, victims) ⇒ Object
-
#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 tonew_mtime
.
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_relationships ⇒ Object
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. = 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, *].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 .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_models ⇒ Object
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_dependencies ⇒ Object
697 698 699 |
# File 'backend/app/model/mixins/relationships.rb', line 697 def relationship_dependencies @relationship_dependencies end |
#relationships ⇒ Object
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 |
#touch_mtime_of_anyone_related_to(obj) ⇒ Object
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 (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 |