module Sequel class Model # Associations are used in order to specify relationships between model classes # that reflect relations between tables in the database using foreign keys. module Associations # Map of association type symbols to association reflection classes. ASSOCIATION_TYPES = {} # Set an empty association reflection hash in the model def self.apply(model) model.instance_variable_set(:@association_reflections, {}) end # AssociationReflection is a Hash subclass that keeps information on Sequel::Model associations. It # provides methods to reduce internal code duplication. It should not # be instantiated by the user. class AssociationReflection < Hash include Sequel::Inflections # Name symbol for the _add internal association method def _add_method :"_add_#{singularize(self[:name])}" end # Name symbol for the _remove_all internal association method def _remove_all_method :"_remove_all_#{self[:name]}" end # Name symbol for the _remove internal association method def _remove_method :"_remove_#{singularize(self[:name])}" end # Name symbol for the _setter association method def _setter_method :"_#{self[:name]}=" end # Name symbol for the add association method def add_method :"add_#{singularize(self[:name])}" end # Name symbol for association method, the same as the name of the association. def association_method self[:name] end # The class associated to the current model class via this association def associated_class cached_fetch(:class){constantize(self[:class_name])} end # The dataset associated via this association, with the non-instance specific # changes already applied. def associated_dataset cached_fetch(:_dataset){apply_dataset_changes(associated_class.dataset.clone)} end # Apply all non-instance specific changes to the given dataset and return it. def apply_dataset_changes(ds) ds.extend(AssociationDatasetMethods) ds.association_reflection = self self[:extend].each{|m| ds.extend(m)} ds = ds.select(*select) if select if c = self[:conditions] ds = (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.where(*c) : ds.where(c) end ds = ds.order(*self[:order]) if self[:order] ds = ds.limit(*self[:limit]) if self[:limit] ds = ds.limit(1) if !returns_array? && self[:key] ds = ds.eager(*self[:eager]) if self[:eager] ds = ds.distinct if self[:distinct] ds end # Whether this association can have associated objects, given the current # object. Should be false if obj cannot have associated objects because # the necessary key columns are NULL. def can_have_associated_objects?(obj) true end # Name symbol for the dataset association method def dataset_method :"#{self[:name]}_dataset" end # Whether the dataset needs a primary key to function, true by default. def dataset_need_primary_key? true end # The eager limit strategy to use for this dataset. def eager_limit_strategy cached_fetch(:_eager_limit_strategy) do if self[:limit] case s = cached_fetch(:eager_limit_strategy){self[:model].default_eager_limit_strategy || :ruby} when true ds = associated_class.dataset if ds.supports_window_functions? :window_function else :ruby end else s end else nil end end end # The key to use for the key hash when eager loading def eager_loader_key self[:eager_loader_key] end # By default associations do not need to select a key in an associated table # to eagerly load. def eager_loading_use_associated_key? false end # Alias of predicate_key, only for backwards compatibility. def eager_loading_predicate_key predicate_key end # Whether to eagerly graph a lazy dataset, true by default. If this # is false, the association won't respect the :eager_graph option # when loading the association for a single record. def eager_graph_lazy_dataset? true end # The limit and offset for this association (returned as a two element array). def limit_and_offset if (v = self[:limit]).is_a?(Array) v else [v, nil] end end # Whether the associated object needs a primary key to be added/removed, # false by default. def need_associated_primary_key? false end # The keys to use for loading of the regular dataset, as an array. def predicate_keys cached_fetch(:predicate_keys){Array(predicate_key)} end # Qualify +col+ with the given table name. If +col+ is an array of columns, # return an array of qualified columns. Only qualifies Symbols and SQL::Identifier # values, other values are not modified. def qualify(table, col) transform(col) do |k| case k when Symbol, SQL::Identifier SQL::QualifiedIdentifier.new(table, k) else Sequel::Qualifier.new(self[:model].dataset, table).transform(k) end end end # Qualify col with the associated model's table name. def qualify_assoc(col) qualify(associated_class.table_name, col) end # Qualify col with the current model's table name. def qualify_cur(col) qualify(self[:model].table_name, col) end # Returns the reciprocal association variable, if one exists. The reciprocal # association is the association in the associated class that is the opposite # of the current association. For example, Album.many_to_one :artist and # Artist.one_to_many :albums are reciprocal associations. This information is # to populate reciprocal associations. For example, when you do this_artist.add_album(album) # it sets album.artist to this_artist. def reciprocal cached_fetch(:reciprocal) do r_types = Array(reciprocal_type) keys = self[:keys] recip = nil associated_class.all_association_reflections.each do |assoc_reflect| if r_types.include?(assoc_reflect[:type]) && assoc_reflect[:keys] == keys && assoc_reflect.associated_class == self[:model] cached_set(:reciprocal_type, assoc_reflect[:type]) recip = assoc_reflect[:name] break end end recip end end # Whether the reciprocal of this association returns an array of objects instead of a single object, # true by default. def reciprocal_array? true end # Name symbol for the remove_all_ association method def remove_all_method :"remove_all_#{self[:name]}" end # Whether associated objects need to be removed from the association before # being destroyed in order to preserve referential integrity. def remove_before_destroy? true end # Name symbol for the remove_ association method def remove_method :"remove_#{singularize(self[:name])}" end # Whether to check that an object to be disassociated is already associated to this object, false by default. def remove_should_check_existing? false end # Whether this association returns an array of objects instead of a single object, # true by default. def returns_array? true end # The columns to select when loading the association. def select self[:select] end # Whether to set the reciprocal association to self when loading associated # records, false by default. def set_reciprocal_to_self? false end # Name symbol for the setter association method def setter_method :"#{self[:name]}=" end private if defined?(RUBY_ENGINE) && RUBY_ENGINE != 'ruby' # :nocov: # On non-GVL rubies, assume the need to synchronize access. Store the key # in a special sub-hash that always uses this method to synchronize access. def cached_fetch(key) fetch(key) do h = self[:cache] Sequel.synchronize{return h[key] if h.has_key?(key)} value = yield Sequel.synchronize{h[key] = value} end end # Cache the value at the given key, synchronizing access. def cached_set(key, value) h = self[:cache] Sequel.synchronize{h[key] = value} end # :nocov: else # On MRI, use a plain fetch, since the GVL will synchronize access. def cached_fetch(key) fetch(key) do h = self[:cache] h.fetch(key){h[key] = yield} end end # On MRI, just set the value at the key in the cache, since the GVL # will synchronize access. def cached_set(key, value) self[:cache][key] = value end end # If +s+ is an array, map +s+ over the block. Otherwise, just call the # block with +s+. def transform(s) s.is_a?(Array) ? s.map(&Proc.new) : (yield s) end end class ManyToOneAssociationReflection < AssociationReflection ASSOCIATION_TYPES[:many_to_one] = self # many_to_one associations can only have associated objects if none of # the :keys options have a nil value. def can_have_associated_objects?(obj) !self[:keys].any?{|k| obj.send(k).nil?} end # Whether the dataset needs a primary key to function, false for many_to_one associations. def dataset_need_primary_key? false end # Default foreign key name symbol for foreign key in current model's table that points to # the given association's table's primary key. def default_key :"#{self[:name]}_id" end # Whether to eagerly graph a lazy dataset, true for many_to_one associations # only if the key is nil. def eager_graph_lazy_dataset? self[:key].nil? end # many_to_one associations don't need an eager limit strategy def eager_limit_strategy nil end # The expression to use on the left hand side of the IN lookup when eager loading def predicate_key cached_fetch(:predicate_key){qualified_primary_key} end # The column(s) in the associated table that the key in the current table references (either a symbol or an array). def primary_key cached_fetch(:primary_key){associated_class.primary_key} end # The columns in the associated table that the key in the current table references (always an array). def primary_keys cached_fetch(:primary_keys){Array(primary_key)} end alias associated_object_keys primary_keys # The method symbol or array of method symbols to call on the associated object # to get the value to use for the foreign keys. def primary_key_method cached_fetch(:primary_key_method){primary_key} end # The array of method symbols to call on the associated object # to get the value to use for the foreign keys. def primary_key_methods cached_fetch(:primary_key_methods){Array(primary_key_method)} end # #primary_key qualified by the associated table def qualified_primary_key cached_fetch(:qualified_primary_key){self[:qualify] == false ? primary_key : qualify_assoc(primary_key)} end # True only if the reciprocal is a one_to_many association. def reciprocal_array? !set_reciprocal_to_self? end # Whether this association returns an array of objects instead of a single object, # false for a many_to_one association. def returns_array? false end # True only if the reciprocal is a one_to_one association. def set_reciprocal_to_self? reciprocal reciprocal_type == :one_to_one end private # The reciprocal type of a many_to_one association is either # a one_to_many or a one_to_one association. def reciprocal_type cached_fetch(:reciprocal_type){[:one_to_many, :one_to_one]} end end class OneToManyAssociationReflection < AssociationReflection ASSOCIATION_TYPES[:one_to_many] = self # The keys in the associated model's table related to this association def associated_object_keys self[:keys] end # one_to_many associations can only have associated objects if none of # the :keys options have a nil value. def can_have_associated_objects?(obj) !self[:primary_keys].any?{|k| obj.send(k).nil?} end # Default foreign key name symbol for key in associated table that points to # current table's primary key. def default_key :"#{underscore(demodulize(self[:model].name))}_id" end # The hash key to use for the eager loading predicate (left side of IN (1, 2, 3)) def predicate_key cached_fetch(:predicate_key){qualify_assoc(self[:key])} end alias qualified_key predicate_key # The column in the current table that the key in the associated table references. def primary_key self[:primary_key] end # #primary_key qualified by the current table def qualified_primary_key cached_fetch(:qualified_primary_key){qualify_cur(primary_key)} end # Whether the reciprocal of this association returns an array of objects instead of a single object, # false for a one_to_many association. def reciprocal_array? false end # Destroying one_to_many associated objects automatically deletes the foreign key. def remove_before_destroy? false end # The one_to_many association needs to check that an object to be removed already is associated. def remove_should_check_existing? true end # One to many associations set the reciprocal to self when loading associated records. def set_reciprocal_to_self? true end private # The reciprocal type of a one_to_many association is a many_to_one association. def reciprocal_type :many_to_one end end class OneToOneAssociationReflection < OneToManyAssociationReflection ASSOCIATION_TYPES[:one_to_one] = self # one_to_one associations don't use an eager limit strategy by default, but # support both DISTINCT ON and window functions as strategies. def eager_limit_strategy cached_fetch(:_eager_limit_strategy) do case s = self[:eager_limit_strategy] when Symbol s when true ds = associated_class.dataset if ds.supports_ordered_distinct_on? :distinct_on elsif ds.supports_window_functions? :window_function end else nil end end end # The limit and offset for this association (returned as a two element array). def limit_and_offset [1, nil] end # one_to_one associations return a single object, not an array def returns_array? false end end class ManyToManyAssociationReflection < AssociationReflection ASSOCIATION_TYPES[:many_to_many] = self # The alias to use for the associated key when eagerly loading def associated_key_alias self[:left_key_alias] end # The column to use for the associated key when eagerly loading def associated_key_column self[:left_key] end # Alias of right_primary_keys def associated_object_keys right_primary_keys end # many_to_many associations can only have associated objects if none of # the :left_primary_keys options have a nil value. def can_have_associated_objects?(obj) !self[:left_primary_keys].any?{|k| obj.send(k).nil?} end # The default associated key alias(es) to use when eager loading # associations via eager. def default_associated_key_alias self[:uses_left_composite_keys] ? (0...self[:left_keys].length).map{|i| :"x_foreign_key_#{i}_x"} : :x_foreign_key_x end # Default name symbol for the join table. def default_join_table [self[:class_name], self[:model].name].map{|i| underscore(pluralize(demodulize(i)))}.sort.join('_').to_sym end # Default foreign key name symbol for key in join table that points to # current table's primary key (or :left_primary_key column). def default_left_key :"#{underscore(demodulize(self[:model].name))}_id" end # Default foreign key name symbol for foreign key in join table that points to # the association's table's primary key (or :right_primary_key column). def default_right_key :"#{singularize(self[:name])}_id" end # The hash key to use for the eager loading predicate (left side of IN (1, 2, 3)). # The left key qualified by the join table. def predicate_key cached_fetch(:predicate_key){qualify(join_table_alias, self[:left_key])} end alias qualified_left_key predicate_key # The right key qualified by the join table. def qualified_right_key cached_fetch(:qualified_right_key){qualify(join_table_alias, self[:right_key])} end # many_to_many associations need to select a key in an associated table to eagerly load def eager_loading_use_associated_key? true end # The source of the join table. This is the join table itself, unless it # is aliased, in which case it is the unaliased part. def join_table_source cached_fetch(:join_table_source){split_join_table_alias[0]} end # The join table itself, unless it is aliased, in which case this # is the alias. def join_table_alias cached_fetch(:join_table_alias) do s, a = split_join_table_alias a || s end end alias associated_key_table join_table_alias # Whether the associated object needs a primary key to be added/removed, # true for many_to_many associations. def need_associated_primary_key? true end # Returns the reciprocal association symbol, if one exists. def reciprocal cached_fetch(:reciprocal) do left_keys = self[:left_keys] right_keys = self[:right_keys] join_table = self[:join_table] recip = nil associated_class.all_association_reflections.each do |assoc_reflect| if assoc_reflect[:type] == :many_to_many && assoc_reflect[:left_keys] == right_keys && assoc_reflect[:right_keys] == left_keys && assoc_reflect[:join_table] == join_table && assoc_reflect.associated_class == self[:model] recip = assoc_reflect[:name] break end end recip end end # #right_primary_key qualified by the associated table def qualified_right_primary_key cached_fetch(:qualified_right_primary_key){qualify_assoc(right_primary_key)} end # The primary key column(s) to use in the associated table (can be symbol or array). def right_primary_key cached_fetch(:right_primary_key){associated_class.primary_key} end # The primary key columns to use in the associated table (always array). def right_primary_keys cached_fetch(:right_primary_keys){Array(right_primary_key)} end # The method symbol or array of method symbols to call on the associated objects # to get the foreign key values for the join table. def right_primary_key_method cached_fetch(:right_primary_key_method){right_primary_key} end # The array of method symbols to call on the associated objects # to get the foreign key values for the join table. def right_primary_key_methods cached_fetch(:right_primary_key_methods){Array(right_primary_key_method)} end # The columns to select when loading the association, associated_class.table_name.* by default. def select cached_fetch(:select){Sequel::SQL::ColumnAll.new(associated_class.table_name)} end private # Split the join table into source and alias parts. def split_join_table_alias associated_class.dataset.split_alias(self[:join_table]) end end # This module contains methods added to all association datasets module AssociationDatasetMethods # The model object that created the association dataset attr_accessor :model_object # The association reflection related to the association dataset attr_accessor :association_reflection end # Each kind of association adds a number of instance methods to the model class which # are specialized according to the association type and optional parameters # given in the definition. Example: # # class Project < Sequel::Model # many_to_one :portfolio # # or: one_to_one :portfolio # one_to_many :milestones # # or: many_to_many :milestones # end # # The project class now has the following instance methods: # portfolio :: Returns the associated portfolio. # portfolio=(obj) :: Sets the associated portfolio to the object, # but the change is not persisted until you save the record (for many_to_one associations). # portfolio_dataset :: Returns a dataset that would return the associated # portfolio, only useful in fairly specific circumstances. # milestones :: Returns an array of associated milestones # add_milestone(obj) :: Associates the passed milestone with this object. # remove_milestone(obj) :: Removes the association with the passed milestone. # remove_all_milestones :: Removes associations with all associated milestones. # milestones_dataset :: Returns a dataset that would return the associated # milestones, allowing for further filtering/limiting/etc. # # If you want to override the behavior of the add_/remove_/remove_all_/ methods # or the association setter method, there are private instance methods created that are prepended # with an underscore (e.g. _add_milestone or _portfolio=). The private instance methods can be # easily overridden, but you shouldn't override the public instance methods without # calling super, as they deal with callbacks and caching. # # By default the classes for the associations are inferred from the association # name, so for example the Project#portfolio will return an instance of # Portfolio, and Project#milestones will return an array of Milestone # instances. You can use the :class option to change which class is used. # # Association definitions are also reflected by the class, e.g.: # # Project.associations # => [:portfolio, :milestones] # Project.association_reflection(:portfolio) # => {:type => :many_to_one, :name => :portfolio, ...} # # Associations should not have the same names as any of the columns in the # model's current table they reference. If you are dealing with an existing schema that # has a column named status, you can't name the association status, you'd # have to name it foo_status or something else. If you give an association the same name # as a column, you will probably end up with an association that doesn't work, or a SystemStackError. # # For a more in depth general overview, as well as a reference guide, # see the {Association Basics guide}[link:files/doc/association_basics_rdoc.html]. # For examples of advanced usage, see the {Advanced Associations guide}[link:files/doc/advanced_associations_rdoc.html]. module ClassMethods # All association reflections defined for this model (default: {}). attr_reader :association_reflections # The default :eager_limit_strategy option to use for *_many associations (default: nil) attr_accessor :default_eager_limit_strategy # Array of all association reflections for this model class def all_association_reflections association_reflections.values end # Given an association reflection and a dataset, apply the # :select, :conditions, :order, :eager, :distinct, and :eager_block # association options to the given dataset and return the dataset # or a modified copy of it. def apply_association_dataset_opts(opts, ds) ds = ds.select(*opts.select) if opts.select if c = opts[:conditions] ds = (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.where(*c) : ds.where(c) end ds = ds.order(*opts[:order]) if opts[:order] ds = ds.eager(opts[:eager]) if opts[:eager] ds = ds.distinct if opts[:distinct] ds = opts[:eager_block].call(ds) if opts[:eager_block] ds end # Associates a related model with the current model. The following types are # supported: # # :many_to_one :: Foreign key in current model's table points to # associated model's primary key. Each associated model object can # be associated with more than one current model objects. Each current # model object can be associated with only one associated model object. # :one_to_many :: Foreign key in associated model's table points to this # model's primary key. Each current model object can be associated with # more than one associated model objects. Each associated model object # can be associated with only one current model object. # :one_to_one :: Similar to one_to_many in terms of foreign keys, but # only one object is associated to the current object through the # association. The methods created are similar to many_to_one, except # that the one_to_one setter method saves the passed object. # :many_to_many :: A join table is used that has a foreign key that points # to this model's primary key and a foreign key that points to the # associated model's primary key. Each current model object can be # associated with many associated model objects, and each associated # model object can be associated with many current model objects. # # The following options can be supplied: # === All Types # :after_add :: Symbol, Proc, or array of both/either specifying a callback to call # after a new item is added to the association. # :after_load :: Symbol, Proc, or array of both/either specifying a callback to call # after the associated record(s) have been retrieved from the database. # :after_remove :: Symbol, Proc, or array of both/either specifying a callback to call # after an item is removed from the association. # :after_set :: Symbol, Proc, or array of both/either specifying a callback to call # after an item is set using the association setter method. # :allow_eager :: If set to false, you cannot load the association eagerly # via eager or eager_graph # :before_add :: Symbol, Proc, or array of both/either specifying a callback to call # before a new item is added to the association. # :before_remove :: Symbol, Proc, or array of both/either specifying a callback to call # before an item is removed from the association. # :before_set :: Symbol, Proc, or array of both/either specifying a callback to call # before an item is set using the association setter method. # :cartesian_product_number :: the number of joins completed by this association that could cause more # than one row for each row in the current table (default: 0 for # many_to_one and one_to_one associations, 1 for one_to_many and # many_to_many associations). # :class :: The associated class or its name as a string or symbol. If not # given, uses the association's name, which is camelized (and # singularized unless the type is :many_to_one or :one_to_one). If this is specified # as a string or symbol, you must specify the full class name (e.g. "SomeModule::MyModel"). # :clone :: Merge the current options and block into the options and block used in defining # the given association. Can be used to DRY up a bunch of similar associations that # all share the same options such as :class and :key, while changing the order and block used. # :conditions :: The conditions to use to filter the association, can be any argument passed to where. # :dataset :: A proc that is instance_execed to get the base dataset to use (before the other # options are applied). If the proc accepts an argument, it is passed the related # association reflection. # :distinct :: Use the DISTINCT clause when selecting associating object, both when # lazy loading and eager loading via .eager (but not when using .eager_graph). # :eager :: The associations to eagerly load via +eager+ when loading the associated object(s). # :eager_block :: If given, use the block instead of the default block when # eagerly loading. To not use a block when eager loading (when one is used normally), # set to nil. # :eager_graph :: The associations to eagerly load via +eager_graph+ when loading the associated object(s). # many_to_many associations with this option cannot be eagerly loaded via +eager+. # :eager_grapher :: A proc to use to implement eager loading via +eager_graph+, overriding the default. # Takes one or three arguments. If three arguments, they are a dataset, an alias to use for # the table to graph for this association, and the alias that was used for the current table # (since you can cascade associations). If one argument, is passed a hash with keys :self, # :table_alias, and :implicit_qualifier, corresponding to the three arguments, and an optional # additional key :eager_block, a callback accepting one argument, the associated dataset. This # is used to customize the association at query time. # Should return a copy of the dataset with the association graphed into it. # :eager_limit_strategy :: Determines the strategy used for enforcing limits when eager loading associations via # the +eager+ method. For one_to_one associations, no strategy is used by default, and # for *_many associations, the :ruby strategy is used by default, which still retrieves # all records but slices the resulting array after the association is retrieved. You # can pass a +true+ value for this option to have Sequel pick what it thinks is the best # choice for the database, or specify a specific symbol to manually select a strategy. # one_to_one associations support :distinct_on, :window_function, and :correlated_subquery. # *_many associations support :ruby, :window_function, and :correlated_subquery. # :eager_loader :: A proc to use to implement eager loading, overriding the default. Takes a single hash argument, # with at least the keys: :rows, which is an array of current model instances, :associations, # which is a hash of dependent associations, :self, which is the dataset doing the eager loading, # :eager_block, which is a dynamic callback that should be called with the dataset, and :id_map, # which is a mapping of key values to arrays of current model instances. In the proc, the # associated records should be queried from the database and the associations cache for each # record should be populated. # :eager_loader_key :: A symbol for the key column to use to populate the key_hash # for the eager loader. Can be set to nil to not populate the key_hash. # :extend :: A module or array of modules to extend the dataset with. # :graph_alias_base :: The base name to use for the table alias when eager graphing. Defaults to the name # of the association. If the alias name has already been used in the query, Sequel will create # a unique alias by appending a numeric suffix (e.g. alias_0, alias_1, ...) until the alias is # unique. # :graph_block :: The block to pass to join_table when eagerly loading # the association via +eager_graph+. # :graph_conditions :: The additional conditions to use on the SQL join when eagerly loading # the association via +eager_graph+. Should be a hash or an array of two element arrays. If not # specified, the :conditions option is used if it is a hash or array of two element arrays. # :graph_join_type :: The type of SQL join to use when eagerly loading the association via # eager_graph. Defaults to :left_outer. # :graph_only_conditions :: The conditions to use on the SQL join when eagerly loading # the association via +eager_graph+, instead of the default conditions specified by the # foreign/primary keys. This option causes the :graph_conditions option to be ignored. # :graph_select :: A column or array of columns to select from the associated table # when eagerly loading the association via +eager_graph+. Defaults to all # columns in the associated table. # :limit :: Limit the number of records to the provided value. Use # an array with two elements for the value to specify a # limit (first element) and an offset (second element). # :methods_module :: The module that methods the association creates will be placed into. Defaults # to the module containing the model's columns. # :order :: the column(s) by which to order the association dataset. Can be a # singular column symbol or an array of column symbols. # :order_eager_graph :: Whether to add the association's order to the graphed dataset's order when graphing # via +eager_graph+. Defaults to true, so set to false to disable. # :read_only :: Do not add a setter method (for many_to_one or one_to_one associations), # or add_/remove_/remove_all_ methods (for one_to_many and many_to_many associations). # :reciprocal :: the symbol name of the reciprocal association, # if it exists. By default, Sequel will try to determine it by looking at the # associated model's assocations for a association that matches # the current association's key(s). Set to nil to not use a reciprocal. # :select :: the columns to select. Defaults to the associated class's # table_name.* in a many_to_many association, which means it doesn't include the attributes from the # join table. If you want to include the join table attributes, you can # use this option, but beware that the join table attributes can clash with # attributes from the model table, so you should alias any attributes that have # the same name in both the join table and the associated table. # :validate :: Set to false to not validate when implicitly saving any associated object. # === :many_to_one # :key :: foreign key in current model's table that references # associated model's primary key, as a symbol. Defaults to :"#{name}_id". Can use an # array of symbols for a composite key association. # :key_column :: Similar to, and usually identical to, :key, but :key refers to the model method # to call, where :key_column refers to the underlying column. Should only be # used if the the model method differs from the foreign key column, in conjunction # with defining a model alias method for the key column. # :primary_key :: column in the associated table that :key option references, as a symbol. # Defaults to the primary key of the associated table. Can use an # array of symbols for a composite key association. # :primary_key_method :: the method symbol or array of method symbols to call on the associated # object to get the foreign key values. Defaults to :primary_key option. # :qualify :: Whether to use qualifier primary keys when loading the association. The default # is true, so you must set to false to not qualify. Qualification rarely causes # problems, but it's necessary to disable in some cases, such as when you are doing # a JOIN USING operation on the column on Oracle. # === :one_to_many and :one_to_one # :key :: foreign key in associated model's table that references # current model's primary key, as a symbol. Defaults to # :"#{self.name.underscore}_id". Can use an # array of symbols for a composite key association. # :key_method :: the method symbol or array of method symbols to call on the associated # object to get the foreign key values. Defaults to :key option. # :primary_key :: column in the current table that :key option references, as a symbol. # Defaults to primary key of the current table. Can use an # array of symbols for a composite key association. # :primary_key_column :: Similar to, and usually identical to, :primary_key, but :primary_key refers # to the model method call, where :primary_key_column refers to the underlying column. # Should only be used if the the model method differs from the primary key column, in # conjunction with defining a model alias method for the primary key column. # === :many_to_many # :graph_join_table_block :: The block to pass to +join_table+ for # the join table when eagerly loading the association via +eager_graph+. # :graph_join_table_conditions :: The additional conditions to use on the SQL join for # the join table when eagerly loading the association via +eager_graph+. # Should be a hash or an array of two element arrays. # :graph_join_table_join_type :: The type of SQL join to use for the join table when eagerly # loading the association via +eager_graph+. Defaults to the # :graph_join_type option or :left_outer. # :graph_join_table_only_conditions :: The conditions to use on the SQL join for the join # table when eagerly loading the association via +eager_graph+, # instead of the default conditions specified by the # foreign/primary keys. This option causes the # :graph_join_table_conditions option to be ignored. # :join_table :: name of table that includes the foreign keys to both # the current model and the associated model, as a symbol. Defaults to the name # of current model and name of associated model, pluralized, # underscored, sorted, and joined with '_'. # :join_table_block :: proc that can be used to modify the dataset used in the add/remove/remove_all # methods. Should accept a dataset argument and return a modified dataset if present. # :left_key :: foreign key in join table that points to current model's # primary key, as a symbol. Defaults to :"#{self.name.underscore}_id". # Can use an array of symbols for a composite key association. # :left_primary_key :: column in current table that :left_key points to, as a symbol. # Defaults to primary key of current table. Can use an # array of symbols for a composite key association. # :left_primary_key_column :: Similar to, and usually identical to, :left_primary_key, but :left_primary_key refers to # the model method to call, where :left_primary_key_column refers to the underlying column. Should only # be used if the model method differs from the left primary key column, in conjunction # with defining a model alias method for the left primary key column. # :right_key :: foreign key in join table that points to associated # model's primary key, as a symbol. Defaults to :"#{name.to_s.singularize}_id". # Can use an array of symbols for a composite key association. # :right_primary_key :: column in associated table that :right_key points to, as a symbol. # Defaults to primary key of the associated table. Can use an # array of symbols for a composite key association. # :right_primary_key_method :: the method symbol or array of method symbols to call on the associated # object to get the foreign key values for the join table. # Defaults to :right_primary_key option. # :uniq :: Adds a after_load callback that makes the array of objects unique. def associate(type, name, opts = {}, &block) raise(Error, 'one_to_many association type with :one_to_one option removed, used one_to_one association type') if opts[:one_to_one] && type == :one_to_many raise(Error, 'invalid association type') unless assoc_class = ASSOCIATION_TYPES[type] raise(Error, 'Model.associate name argument must be a symbol') unless Symbol === name raise(Error, ':eager_loader option must have an arity of 1 or 3') if opts[:eager_loader] && ![1, 3].include?(opts[:eager_loader].arity) raise(Error, ':eager_grapher option must have an arity of 1 or 3') if opts[:eager_grapher] && ![1, 3].include?(opts[:eager_grapher].arity) # dup early so we don't modify opts orig_opts = opts.dup if opts[:clone] cloned_assoc = association_reflection(opts[:clone]) raise(Error, "cannot clone an association to an association of different type (association #{name} with type #{type} cloning #{opts[:clone]} with type #{cloned_assoc[:type]})") unless cloned_assoc[:type] == type || [cloned_assoc[:type], type].all?{|t| [:one_to_many, :one_to_one].include?(t)} orig_opts = cloned_assoc[:orig_opts].merge(orig_opts) end opts = orig_opts.merge(:type => type, :name => name, :cache=>{}, :model => self) opts[:block] = block if block opts = assoc_class.new.merge!(opts) opts[:eager_block] = block unless opts.include?(:eager_block) if !opts.has_key?(:predicate_key) && opts.has_key?(:eager_loading_predicate_key) opts[:predicate_key] = opts[:eager_loading_predicate_key] end opts[:graph_join_type] ||= :left_outer opts[:order_eager_graph] = true unless opts.include?(:order_eager_graph) conds = opts[:conditions] opts[:graph_alias_base] ||= name opts[:graph_conditions] = conds if !opts.include?(:graph_conditions) and Sequel.condition_specifier?(conds) opts[:graph_conditions] = opts.fetch(:graph_conditions, []).to_a opts[:graph_select] = Array(opts[:graph_select]) if opts[:graph_select] [:before_add, :before_remove, :after_add, :after_remove, :after_load, :before_set, :after_set, :extend].each do |cb_type| opts[cb_type] = Array(opts[cb_type]) end late_binding_class_option(opts, opts.returns_array? ? singularize(name) : name) # Remove :class entry if it exists and is nil, to work with cached_fetch opts.delete(:class) unless opts[:class] send(:"def_#{type}", opts) orig_opts.delete(:clone) orig_opts.merge!(:class_name=>opts[:class_name], :class=>opts[:class], :block=>block) opts[:orig_opts] = orig_opts # don't add to association_reflections until we are sure there are no errors association_reflections[name] = opts end # The association reflection hash for the association of the given name. def association_reflection(name) association_reflections[name] end # Array of association name symbols def associations association_reflections.keys end # Modify and return eager loading dataset based on association options. def eager_loading_dataset(opts, ds, select, associations, eager_options={}) ds = apply_association_dataset_opts(opts, ds) ds = ds.select(*select) if select if opts[:eager_graph] raise(Error, "cannot eagerly load a #{opts[:type]} association that uses :eager_graph") if opts.eager_loading_use_associated_key? ds = ds.eager_graph(opts[:eager_graph]) end ds = ds.eager(associations) unless Array(associations).empty? ds = eager_options[:eager_block].call(ds) if eager_options[:eager_block] if opts.eager_loading_use_associated_key? ds = if opts[:uses_left_composite_keys] ds.select_append(*opts.associated_key_alias.zip(opts.predicate_keys).map{|a, k| SQL::AliasedExpression.new(k, a)}) else ds.select_append(SQL::AliasedExpression.new(opts.predicate_key, opts.associated_key_alias)) end end ds end # Copy the association reflections to the subclass def inherited(subclass) super subclass.instance_variable_set(:@association_reflections, association_reflections.dup) subclass.default_eager_limit_strategy = default_eager_limit_strategy end # Shortcut for adding a many_to_many association, see #associate def many_to_many(name, opts={}, &block) associate(:many_to_many, name, opts, &block) end # Shortcut for adding a many_to_one association, see #associate def many_to_one(name, opts={}, &block) associate(:many_to_one, name, opts, &block) end # Shortcut for adding a one_to_many association, see #associate def one_to_many(name, opts={}, &block) associate(:one_to_many, name, opts, &block) end # Shortcut for adding a one_to_one association, see #associate. def one_to_one(name, opts={}, &block) associate(:one_to_one, name, opts, &block) end private # Use a correlated subquery to limit the results of the eager loading dataset. def apply_correlated_subquery_eager_limit_strategy(ds, opts) klass = opts.associated_class kds = klass.dataset dsa = ds.send(:dataset_alias, 1) raise Error, "can't use a correlated subquery if the associated class (#{opts.associated_class.inspect}) does not have a primary key" unless pk = klass.primary_key pka = Array(pk) raise Error, "can't use a correlated subquery if the associated class (#{opts.associated_class.inspect}) has a composite primary key and the database does not support multiple column IN" if pka.length > 1 && !ds.supports_multiple_column_in? table = kds.opts[:from] raise Error, "can't use a correlated subquery unless the associated class (#{opts.associated_class.inspect}) uses a single FROM table" unless table && table.length == 1 table = table.first if order = ds.opts[:order] oproc = lambda do |x| case x when Symbol t, c, a = ds.send(:split_symbol, x) if t && t.to_sym == table SQL::QualifiedIdentifier.new(dsa, c) else x end when SQL::QualifiedIdentifier if x.table == table SQL::QualifiedIdentifier.new(dsa, x.column) else x end when SQL::OrderedExpression SQL::OrderedExpression.new(oproc.call(x.expression), x.descending, :nulls=>x.nulls) else x end end order = order.map(&oproc) end limit, offset = opts.limit_and_offset subquery = yield kds. unlimited. from(SQL::AliasedExpression.new(table, dsa)). select(*pka.map{|k| SQL::QualifiedIdentifier.new(dsa, k)}). order(*order). limit(limit, offset) pk = if pk.is_a?(Array) pk.map{|k| SQL::QualifiedIdentifier.new(table, k)} else SQL::QualifiedIdentifier.new(table, pk) end ds.where(pk=>subquery) end # Use a window function to limit the results of the eager loading dataset. def apply_window_function_eager_limit_strategy(ds, opts) rn = ds.row_number_column limit, offset = opts.limit_and_offset ds = ds.unordered.select_append{row_number(:over, :partition=>opts.predicate_key, :order=>ds.opts[:order]){}.as(rn)}.from_self ds = if opts[:type] == :one_to_one ds.where(rn => 1) elsif offset offset += 1 ds.where(rn => (offset...(offset+limit))) else ds.where{SQL::Identifier.new(rn) <= limit} end end # The module to use for the association's methods. Defaults to # the overridable_methods_module. def association_module(opts={}) opts.fetch(:methods_module, overridable_methods_module) end # Add a method to the module included in the class, so the method # can be easily overridden in the class itself while allowing for # super to be called. def association_module_def(name, opts={}, &block) association_module(opts).module_eval{define_method(name, &block)} end # Add a private method to the module included in the class. def association_module_private_def(name, opts={}, &block) association_module_def(name, opts, &block) association_module(opts).send(:private, name) end # Add the add_ instance method def def_add_method(opts) association_module_def(opts.add_method, opts){|o,*args| add_associated_object(opts, o, *args)} end # Adds the association dataset methods to the association methods module. def def_association_dataset_methods(opts) association_module_def(opts.dataset_method, opts){_dataset(opts)} def_association_method(opts) end # Adds the association method to the association methods module. def def_association_method(opts) association_module_def(opts.association_method, opts){|*dynamic_opts, &block| load_associated_objects(opts, dynamic_opts[0], &block)} end # Configures many_to_many association reflection and adds the related association methods def def_many_to_many(opts) name = opts[:name] model = self left = (opts[:left_key] ||= opts.default_left_key) lcks = opts[:left_keys] = Array(left) right = (opts[:right_key] ||= opts.default_right_key) rcks = opts[:right_keys] = Array(right) left_pk = (opts[:left_primary_key] ||= self.primary_key) opts[:eager_loader_key] = left_pk unless opts.has_key?(:eager_loader_key) lcpks = opts[:left_primary_keys] = Array(left_pk) lpkc = opts[:left_primary_key_column] ||= left_pk lpkcs = opts[:left_primary_key_columns] ||= Array(lpkc) raise(Error, "mismatched number of left keys: #{lcks.inspect} vs #{lcpks.inspect}") unless lcks.length == lcpks.length if opts[:right_primary_key] rcpks = Array(opts[:right_primary_key]) raise(Error, "mismatched number of right keys: #{rcks.inspect} vs #{rcpks.inspect}") unless rcks.length == rcpks.length end uses_lcks = opts[:uses_left_composite_keys] = lcks.length > 1 uses_rcks = opts[:uses_right_composite_keys] = rcks.length > 1 opts[:cartesian_product_number] ||= 1 join_table = (opts[:join_table] ||= opts.default_join_table) left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias graph_jt_conds = opts[:graph_join_table_conditions] = opts.fetch(:graph_join_table_conditions, []).to_a opts[:graph_join_table_join_type] ||= opts[:graph_join_type] opts[:after_load].unshift(:array_uniq!) if opts[:uniq] opts[:dataset] ||= proc{opts.associated_dataset.inner_join(join_table, rcks.zip(opts.right_primary_keys) + opts.predicate_keys.zip(lcpks.map{|k| send(k)}), :qualify=>:deep)} opts[:eager_loader] ||= proc do |eo| h = eo[:id_map] rows = eo[:rows] rows.each{|object| object.associations[name] = []} r = rcks.zip(opts.right_primary_keys) l = [[opts.predicate_key, h.keys]] ds = model.eager_loading_dataset(opts, opts.associated_class.inner_join(join_table, r + l, :qualify=>:deep), nil, eo[:associations], eo) case opts.eager_limit_strategy when :window_function delete_rn = true rn = ds.row_number_column ds = apply_window_function_eager_limit_strategy(ds, opts) when :correlated_subquery ds = apply_correlated_subquery_eager_limit_strategy(ds, opts) do |xds| dsa = ds.send(:dataset_alias, 2) xds.inner_join(join_table, r + lcks.map{|k| [k, SQL::QualifiedIdentifier.new(opts.join_table_alias, k)]}, :table_alias=>dsa, :qualify=>:deep) end end ds.all do |assoc_record| assoc_record.values.delete(rn) if delete_rn hash_key = if uses_lcks left_key_alias.map{|k| assoc_record.values.delete(k)} else assoc_record.values.delete(left_key_alias) end next unless objects = h[hash_key] objects.each{|object| object.associations[name].push(assoc_record)} end if opts.eager_limit_strategy == :ruby limit, offset = opts.limit_and_offset rows.each{|o| o.associations[name] = o.associations[name].slice(offset||0, limit) || []} end end join_type = opts[:graph_join_type] select = opts[:graph_select] use_only_conditions = opts.include?(:graph_only_conditions) only_conditions = opts[:graph_only_conditions] conditions = opts[:graph_conditions] graph_block = opts[:graph_block] use_jt_only_conditions = opts.include?(:graph_join_table_only_conditions) jt_only_conditions = opts[:graph_join_table_only_conditions] jt_join_type = opts[:graph_join_table_join_type] jt_graph_block = opts[:graph_join_table_block] opts[:eager_grapher] ||= proc do |eo| ds = eo[:self] ds = ds.graph(join_table, use_jt_only_conditions ? jt_only_conditions : lcks.zip(lpkcs) + graph_jt_conds, :select=>false, :table_alias=>ds.unused_table_alias(join_table, [eo[:table_alias]]), :join_type=>jt_join_type, :implicit_qualifier=>eo[:implicit_qualifier], :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master], &jt_graph_block) ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.right_primary_keys.zip(rcks) + conditions, :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>join_type, &graph_block) end def_association_dataset_methods(opts) return if opts[:read_only] association_module_private_def(opts._add_method, opts) do |o| h = {} lcks.zip(lcpks).each{|k, pk| h[k] = send(pk)} rcks.zip(opts.right_primary_key_methods).each{|k, pk| h[k] = o.send(pk)} _join_table_dataset(opts).insert(h) end association_module_private_def(opts._remove_method, opts) do |o| _join_table_dataset(opts).where(lcks.zip(lcpks.map{|k| send(k)}) + rcks.zip(opts.right_primary_key_methods.map{|k| o.send(k)})).delete end association_module_private_def(opts._remove_all_method, opts) do _join_table_dataset(opts).where(lcks.zip(lcpks.map{|k| send(k)})).delete end def_add_method(opts) def_remove_methods(opts) end # Configures many_to_one association reflection and adds the related association methods def def_many_to_one(opts) name = opts[:name] model = self opts[:key] = opts.default_key unless opts.has_key?(:key) key = opts[:key] opts[:eager_loader_key] = key unless opts.has_key?(:eager_loader_key) cks = opts[:graph_keys] = opts[:keys] = Array(key) opts[:key_column] ||= key opts[:graph_keys] = opts[:key_columns] = Array(opts[:key_column]) opts[:qualified_key] = opts.qualify_cur(key) if opts[:primary_key] cpks = Array(opts[:primary_key]) raise(Error, "mismatched number of keys: #{cks.inspect} vs #{cpks.inspect}") unless cks.length == cpks.length end uses_cks = opts[:uses_composite_keys] = cks.length > 1 qualify = opts[:qualify] != false opts[:cartesian_product_number] ||= 0 opts[:dataset] ||= proc do opts.associated_dataset.where(opts.predicate_keys.zip(cks.map{|k| send(k)})) end opts[:eager_loader] ||= proc do |eo| h = eo[:id_map] keys = h.keys # Default the cached association to nil, so any object that doesn't have it # populated will have cached the negative lookup. eo[:rows].each{|object| object.associations[name] = nil} # Skip eager loading if no objects have a foreign key for this association unless keys.empty? klass = opts.associated_class model.eager_loading_dataset(opts, klass.where(opts.predicate_key=>keys), nil, eo[:associations], eo).all do |assoc_record| hash_key = uses_cks ? opts.primary_key_methods.map{|k| assoc_record.send(k)} : assoc_record.send(opts.primary_key_method) next unless objects = h[hash_key] objects.each{|object| object.associations[name] = assoc_record} end end end join_type = opts[:graph_join_type] select = opts[:graph_select] use_only_conditions = opts.include?(:graph_only_conditions) only_conditions = opts[:graph_only_conditions] conditions = opts[:graph_conditions] graph_block = opts[:graph_block] graph_cks = opts[:graph_keys] opts[:eager_grapher] ||= proc do |eo| ds = eo[:self] ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.primary_keys.zip(graph_cks) + conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block) end def_association_dataset_methods(opts) return if opts[:read_only] association_module_private_def(opts._setter_method, opts){|o| cks.zip(opts.primary_key_methods).each{|k, pk| send(:"#{k}=", (o.send(pk) if o))}} association_module_def(opts.setter_method, opts){|o| set_associated_object(opts, o)} end # Configures one_to_many and one_to_one association reflections and adds the related association methods def def_one_to_many(opts) one_to_one = opts[:type] == :one_to_one name = opts[:name] model = self key = (opts[:key] ||= opts.default_key) km = opts[:key_method] ||= opts[:key] cks = opts[:keys] = Array(key) opts[:key_methods] = Array(opts[:key_method]) primary_key = (opts[:primary_key] ||= self.primary_key) opts[:eager_loader_key] = primary_key unless opts.has_key?(:eager_loader_key) cpks = opts[:primary_keys] = Array(primary_key) pkc = opts[:primary_key_column] ||= primary_key pkcs = opts[:primary_key_columns] ||= Array(pkc) raise(Error, "mismatched number of keys: #{cks.inspect} vs #{cpks.inspect}") unless cks.length == cpks.length uses_cks = opts[:uses_composite_keys] = cks.length > 1 opts[:dataset] ||= proc do opts.associated_dataset.where(opts.predicate_keys.zip(cpks.map{|k| send(k)})) end opts[:eager_loader] ||= proc do |eo| h = eo[:id_map] rows = eo[:rows] if one_to_one rows.each{|object| object.associations[name] = nil} else rows.each{|object| object.associations[name] = []} end reciprocal = opts.reciprocal klass = opts.associated_class filter_keys = opts.predicate_key ds = model.eager_loading_dataset(opts, klass.where(filter_keys=>h.keys), nil, eo[:associations], eo) case opts.eager_limit_strategy when :distinct_on ds = ds.distinct(*filter_keys).order_prepend(*filter_keys) when :window_function delete_rn = true rn = ds.row_number_column ds = apply_window_function_eager_limit_strategy(ds, opts) when :correlated_subquery ds = apply_correlated_subquery_eager_limit_strategy(ds, opts) do |xds| xds.where(opts.associated_object_keys.map{|k| [SQL::QualifiedIdentifier.new(xds.first_source_alias, k), SQL::QualifiedIdentifier.new(xds.first_source_table, k)]}) end end ds.all do |assoc_record| assoc_record.values.delete(rn) if delete_rn hash_key = uses_cks ? km.map{|k| assoc_record.send(k)} : assoc_record.send(km) next unless objects = h[hash_key] if one_to_one objects.each do |object| unless object.associations[name] object.associations[name] = assoc_record assoc_record.associations[reciprocal] = object if reciprocal end end else objects.each do |object| object.associations[name].push(assoc_record) assoc_record.associations[reciprocal] = object if reciprocal end end end if opts.eager_limit_strategy == :ruby limit, offset = opts.limit_and_offset rows.each{|o| o.associations[name] = o.associations[name].slice(offset||0, limit) || []} end end join_type = opts[:graph_join_type] select = opts[:graph_select] use_only_conditions = opts.include?(:graph_only_conditions) only_conditions = opts[:graph_only_conditions] conditions = opts[:graph_conditions] opts[:cartesian_product_number] ||= one_to_one ? 0 : 1 graph_block = opts[:graph_block] opts[:eager_grapher] ||= proc do |eo| ds = eo[:self] ds = ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : cks.zip(pkcs) + conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block) # We only load reciprocals for one_to_many associations, as other reciprocals don't make sense ds.opts[:eager_graph][:reciprocals][eo[:table_alias]] = opts.reciprocal ds end def_association_dataset_methods(opts) ck_nil_hash ={} cks.each{|k| ck_nil_hash[k] = nil} unless opts[:read_only] validate = opts[:validate] if one_to_one association_module_private_def(opts._setter_method, opts) do |o| up_ds = _apply_association_options(opts, opts.associated_dataset.where(cks.zip(cpks.map{|k| send(k)}))) if o up_ds = up_ds.exclude(o.pk_hash) unless o.new? cks.zip(cpks).each{|k, pk| o.send(:"#{k}=", send(pk))} end checked_transaction do up_ds.update(ck_nil_hash) o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save") if o end end association_module_def(opts.setter_method, opts){|o| set_one_to_one_associated_object(opts, o)} else association_module_private_def(opts._add_method, opts) do |o| cks.zip(cpks).each{|k, pk| o.send(:"#{k}=", send(pk))} o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save") end def_add_method(opts) association_module_private_def(opts._remove_method, opts) do |o| cks.each{|k| o.send(:"#{k}=", nil)} o.save(:validate=>validate) || raise(Sequel::Error, "invalid associated object, cannot save") end association_module_private_def(opts._remove_all_method, opts) do _apply_association_options(opts, opts.associated_dataset.where(cks.zip(cpks.map{|k| send(k)}))).update(ck_nil_hash) end def_remove_methods(opts) end end end # Alias of def_one_to_many, since they share pretty much the same code. def def_one_to_one(opts) def_one_to_many(opts) end # Add the remove_ and remove_all instance methods def def_remove_methods(opts) association_module_def(opts.remove_method, opts){|o,*args| remove_associated_object(opts, o, *args)} association_module_def(opts.remove_all_method, opts){|*args| remove_all_associated_objects(opts, *args)} end # Return dataset to graph into given the association reflection, applying the :callback option if set. def eager_graph_dataset(opts, eager_options) ds = opts.associated_class.dataset if cb = eager_options[:callback] ds = cb.call(ds) end ds end end # Instance methods used to implement the associations support. module InstanceMethods # The currently cached associations. A hash with the keys being the # association name symbols and the values being the associated object # or nil (many_to_one), or the array of associated objects (*_to_many). def associations @associations ||= {} end # Freeze the associations cache when freezing the object. Note that # retrieving associations after freezing will still work in most cases, # but the associations will not be cached in the association cache. def freeze associations.freeze super end # Clear the associations cache when refreshing def set_values(hash) @associations.clear if @associations super end # Formally used internally by the associations code, like pk but doesn't raise # an Error if the model has no primary key. Not used any longer, deprecated. def pk_or_nil key = primary_key key.is_a?(Array) ? key.map{|k| @values[k]} : @values[key] end private # Apply the association options such as :order and :limit to the given dataset, returning a modified dataset. def _apply_association_options(opts, ds) unless ds.kind_of?(AssociationDatasetMethods) ds = opts.apply_dataset_changes(ds) end ds.model_object = self ds = ds.eager_graph(opts[:eager_graph]) if opts[:eager_graph] && opts.eager_graph_lazy_dataset? ds = instance_exec(ds, &opts[:block]) if opts[:block] ds end # Return a dataset for the association after applying any dynamic callback. def _associated_dataset(opts, dynamic_opts) ds = send(opts.dataset_method) if callback = dynamic_opts[:callback] ds = callback.call(ds) end ds end # Return an association dataset for the given association reflection def _dataset(opts) raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk ds = if opts[:dataset].arity == 1 instance_exec(opts, &opts[:dataset]) else instance_exec(&opts[:dataset]) end _apply_association_options(opts, ds) end # Dataset for the join table of the given many to many association reflection def _join_table_dataset(opts) ds = model.db.from(opts.join_table_source) opts[:join_table_block] ? opts[:join_table_block].call(ds) : ds end # Return the associated single object for the given association reflection and dynamic options # (or nil if no associated object). def _load_associated_object(opts, dynamic_opts) _load_associated_object_array(opts, dynamic_opts).first end # Load the associated objects for the given association reflection and dynamic options # as an array. def _load_associated_object_array(opts, dynamic_opts) _associated_dataset(opts, dynamic_opts).all end # Return the associated objects from the dataset, without association callbacks, reciprocals, and caching. # Still apply the dynamic callback if present. def _load_associated_objects(opts, dynamic_opts={}) if opts.can_have_associated_objects?(self) if opts.returns_array? _load_associated_object_array(opts, dynamic_opts) else _load_associated_object(opts, dynamic_opts) end else [] if opts.returns_array? end end # Add the given associated object to the given association def add_associated_object(opts, o, *args) klass = opts.associated_class if o.is_a?(Hash) o = klass.new(o) elsif o.is_a?(Integer) || o.is_a?(String) || o.is_a?(Array) o = klass[o] elsif !o.is_a?(klass) raise(Sequel::Error, "associated object #{o.inspect} not of correct type #{klass}") end raise(Sequel::Error, "model object #{inspect} does not have a primary key") unless pk ensure_associated_primary_key(opts, o, *args) return if run_association_callbacks(opts, :before_add, o) == false send(opts._add_method, o, *args) if array = associations[opts[:name]] and !array.include?(o) array.push(o) end add_reciprocal_object(opts, o) run_association_callbacks(opts, :after_add, o) o end # Add/Set the current object to/as the given object's reciprocal association. def add_reciprocal_object(opts, o) return if o.frozen? return unless reciprocal = opts.reciprocal if opts.reciprocal_array? if array = o.associations[reciprocal] and !array.include?(self) array.push(self) end else o.associations[reciprocal] = self end end # Call uniq! on the given array. This is used by the :uniq option, # and is an actual method for memory reasons. def array_uniq!(a) a.uniq! end # Save the associated object if the associated object needs a primary key # and the associated object is new and does not have one. Raise an error if # the object still does not have a primary key def ensure_associated_primary_key(opts, o, *args) if opts.need_associated_primary_key? o.save(:validate=>opts[:validate]) if o.new? raise(Sequel::Error, "associated object #{o.inspect} does not have a primary key") unless o.pk end end # Load the associated objects using the dataset, handling callbacks, reciprocals, and caching. def load_associated_objects(opts, dynamic_opts=nil) if dynamic_opts == true or dynamic_opts == false or dynamic_opts == nil dynamic_opts = {:reload=>dynamic_opts} elsif dynamic_opts.respond_to?(:call) dynamic_opts = {:callback=>dynamic_opts} end if block_given? dynamic_opts = dynamic_opts.merge(:callback=>Proc.new) end name = opts[:name] if associations.include?(name) and !dynamic_opts[:callback] and !dynamic_opts[:reload] associations[name] else objs = _load_associated_objects(opts, dynamic_opts) if opts.set_reciprocal_to_self? if opts.returns_array? objs.each{|o| add_reciprocal_object(opts, o)} elsif objs add_reciprocal_object(opts, objs) end end # If the current object is frozen, you can't update the associations # cache. This can cause issues for after_load procs that expect # the objects to be already cached in the associations, but # unfortunately that case cannot be handled. associations[name] = objs unless frozen? run_association_callbacks(opts, :after_load, objs) frozen? ? objs : associations[name] end end # Remove all associated objects from the given association def remove_all_associated_objects(opts, *args) raise(Sequel::Error, "model object #{inspect} does not have a primary key") unless pk send(opts._remove_all_method, *args) ret = associations[opts[:name]].each{|o| remove_reciprocal_object(opts, o)} if associations.include?(opts[:name]) associations[opts[:name]] = [] ret end # Remove the given associated object from the given association def remove_associated_object(opts, o, *args) klass = opts.associated_class if o.is_a?(Integer) || o.is_a?(String) || o.is_a?(Array) o = remove_check_existing_object_from_pk(opts, o, *args) elsif !o.is_a?(klass) raise(Sequel::Error, "associated object #{o.inspect} not of correct type #{klass}") elsif opts.remove_should_check_existing? && send(opts.dataset_method).where(o.pk_hash).empty? raise(Sequel::Error, "associated object #{o.inspect} is not currently associated to #{inspect}") end raise(Sequel::Error, "model object #{inspect} does not have a primary key") unless pk raise(Sequel::Error, "associated object #{o.inspect} does not have a primary key") if opts.need_associated_primary_key? && !o.pk return if run_association_callbacks(opts, :before_remove, o) == false send(opts._remove_method, o, *args) associations[opts[:name]].delete_if{|x| o === x} if associations.include?(opts[:name]) remove_reciprocal_object(opts, o) run_association_callbacks(opts, :after_remove, o) o end # Check that the object from the associated table specified by the primary key # is currently associated to the receiver. If it is associated, return the object, otherwise # raise an error. def remove_check_existing_object_from_pk(opts, o, *args) key = o pkh = opts.associated_class.qualified_primary_key_hash(key) raise(Sequel::Error, "no object with key(s) #{key.inspect} is currently associated to #{inspect}") unless o = send(opts.dataset_method).first(pkh) o end # Remove/unset the current object from/as the given object's reciprocal association. def remove_reciprocal_object(opts, o) return unless reciprocal = opts.reciprocal if opts.reciprocal_array? if array = o.associations[reciprocal] array.delete_if{|x| self === x} end else o.associations[reciprocal] = nil end end # Run the callback for the association with the object. def run_association_callbacks(reflection, callback_type, object) raise_error = raise_on_save_failure || !reflection.returns_array? stop_on_false = [:before_add, :before_remove, :before_set].include?(callback_type) reflection[callback_type].each do |cb| res = case cb when Symbol send(cb, object) when Proc cb.call(self, object) else raise Error, "callbacks should either be Procs or Symbols" end if res == false and stop_on_false raise(BeforeHookFailed, "Unable to modify association for #{inspect}: one of the #{callback_type} hooks returned false") if raise_error return false end end end # Set the given object as the associated object for the given *_to_one association reflection def _set_associated_object(opts, o) a = associations[opts[:name]] return if a && a == o && !set_associated_object_if_same? run_association_callbacks(opts, :before_set, o) remove_reciprocal_object(opts, a) if a send(opts._setter_method, o) associations[opts[:name]] = o add_reciprocal_object(opts, o) if o run_association_callbacks(opts, :after_set, o) o end # Whether run the associated object setter code if passed the same object as the one already # cached in the association. Usually not set (so nil), can be set on a per-object basis # if necessary. def set_associated_object_if_same? @set_associated_object_if_same end # Set the given object as the associated object for the given many_to_one association reflection def set_associated_object(opts, o) raise(Error, "associated object #{o.inspect} does not have a primary key") if o && !o.pk _set_associated_object(opts, o) end # Set the given object as the associated object for the given one_to_one association reflection def set_one_to_one_associated_object(opts, o) raise(Error, "object #{inspect} does not have a primary key") unless pk _set_associated_object(opts, o) end end # Eager loading makes it so that you can load all associated records for a # set of objects in a single query, instead of a separate query for each object. # # Two separate implementations are provided. +eager+ should be used most of the # time, as it loads associated records using one query per association. However, # it does not allow you the ability to filter or order based on columns in associated tables. +eager_graph+ loads # all records in a single query using JOINs, allowing you to filter or order based on columns in associated # tables. However, +eager_graph+ is usually slower than +eager+, especially if multiple # one_to_many or many_to_many associations are joined. # # You can cascade the eager loading (loading associations on associated objects) # with no limit to the depth of the cascades. You do this by passing a hash to +eager+ or +eager_graph+ # with the keys being associations of the current model and values being # associations of the model associated with the current model via the key. # # The arguments can be symbols or hashes with symbol keys (for cascaded # eager loading). Examples: # # Album.eager(:artist).all # Album.eager_graph(:artist).all # Album.eager(:artist, :genre).all # Album.eager_graph(:artist, :genre).all # Album.eager(:artist).eager(:genre).all # Album.eager_graph(:artist).eager(:genre).all # Artist.eager(:albums=>:tracks).all # Artist.eager_graph(:albums=>:tracks).all # Artist.eager(:albums=>{:tracks=>:genre}).all # Artist.eager_graph(:albums=>{:tracks=>:genre}).all # # You can also pass a callback as a hash value in order to customize the dataset being # eager loaded at query time, analogous to the way the :eager_block association option # allows you to customize it at association definition time. For example, # if you wanted artists with their albums since 1990: # # Artist.eager(:albums => proc{|ds| ds.where{year > 1990}}) # # Or if you needed albums and their artist's name only, using a single query: # # Albums.eager_graph(:artist => proc{|ds| ds.select(:name)}) # # To cascade eager loading while using a callback, you substitute the cascaded # associations with a single entry hash that has the proc callback as the key and # the cascaded associations as the value. This will load artists with their albums # since 1990, and also the tracks on those albums and the genre for those tracks: # # Artist.eager(:albums => {proc{|ds| ds.where{year > 1990}}=>{:tracks => :genre}}) module DatasetMethods Sequel::Dataset.def_mutation_method(:eager, :eager_graph, :module=>self) # If the expression is in the form x = y where +y+ is a Sequel::Model # instance, array of Sequel::Model instances, or a Sequel::Model dataset, # assume +x+ is an association symbol and look up the association reflection # via the dataset's model. From there, return the appropriate SQL based on the type of # association and the values of the foreign/primary keys of +y+. For most association # types, this is a simple transformation, but for +many_to_many+ associations this # creates a subquery to the join table. def complex_expression_sql_append(sql, op, args) r = args.at(1) if (((op == :'=' || op == :'!=') and r.is_a?(Sequel::Model)) || (multiple = ((op == :IN || op == :'NOT IN') and ((is_ds = r.is_a?(Sequel::Dataset)) or r.all?{|x| x.is_a?(Sequel::Model)})))) l = args.at(0) if ar = model.association_reflections[l] if multiple klass = ar.associated_class if is_ds if r.respond_to?(:model) unless r.model <= klass # A dataset for a different model class, could be a valid regular query return super end else # Not a model dataset, could be a valid regular query return super end else unless r.all?{|x| x.is_a?(klass)} raise Sequel::Error, "invalid association class for one object for association #{l.inspect} used in dataset filter for model #{model.inspect}, expected class #{klass.inspect}" end end elsif !r.is_a?(ar.associated_class) raise Sequel::Error, "invalid association class #{r.class.inspect} for association #{l.inspect} used in dataset filter for model #{model.inspect}, expected class #{ar.associated_class.inspect}" end if exp = association_filter_expression(op, ar, r) literal_append(sql, exp) else raise Sequel::Error, "invalid association type #{ar[:type].inspect} for association #{l.inspect} used in dataset filter for model #{model.inspect}" end elsif multiple && (is_ds || r.empty?) # Not a query designed for this support, could be a valid regular query super else raise Sequel::Error, "invalid association #{l.inspect} used in dataset filter for model #{model.inspect}" end else super end end # The preferred eager loading method. Loads all associated records using one # query for each association. # # The basic idea for how it works is that the dataset is first loaded normally. # Then it goes through all associations that have been specified via +eager+. # It loads each of those associations separately, then associates them back # to the original dataset via primary/foreign keys. Due to the necessity of # all objects being present, you need to use +all+ to use eager loading, as it # can't work with +each+. # # This implementation avoids the complexity of extracting an object graph out # of a single dataset, by building the object graph out of multiple datasets, # one for each association. By using a separate dataset for each association, # it avoids problems such as aliasing conflicts and creating cartesian product # result sets if multiple one_to_many or many_to_many eager associations are requested. # # One limitation of using this method is that you cannot filter the dataset # based on values of columns in an associated table, since the associations are loaded # in separate queries. To do that you need to load all associations in the # same query, and extract an object graph from the results of that query. If you # need to filter based on columns in associated tables, look at +eager_graph+ # or join the tables you need to filter on manually. # # Each association's order, if defined, is respected. # If the association uses a block or has an :eager_block argument, it is used. def eager(*associations) opt = @opts[:eager] opt = opt ? opt.dup : {} associations.flatten.each do |association| case association when Symbol check_association(model, association) opt[association] = nil when Hash association.keys.each{|assoc| check_association(model, assoc)} opt.merge!(association) else raise(Sequel::Error, 'Associations must be in the form of a symbol or hash') end end clone(:eager=>opt) end # The secondary eager loading method. Loads all associations in a single query. This # method should only be used if you need to filter or order based on columns in associated tables. # # This method uses Dataset#graph to create appropriate aliases for columns in all the # tables. Then it uses the graph's metadata to build the associations from the single hash, and # finally replaces the array of hashes with an array model objects inside all. # # Be very careful when using this with multiple one_to_many or many_to_many associations, as you can # create large cartesian products. If you must graph multiple one_to_many and many_to_many associations, # make sure your filters are narrow if you have a large database. # # Each association's order, if definied, is respected. +eager_graph+ probably # won't work correctly on a limited dataset, unless you are # only graphing many_to_one and one_to_one associations. # # Does not use the block defined for the association, since it does a single query for # all objects. You can use the :graph_* association options to modify the SQL query. # # Like +eager+, you need to call +all+ on the dataset for the eager loading to work. If you just # call +each+, it will yield plain hashes, each containing all columns from all the tables. def eager_graph(*associations) ds = if eg = @opts[:eager_graph] eg = eg.dup [:requirements, :reflections, :reciprocals].each{|k| eg[k] = eg[k].dup} clone(:eager_graph=>eg) else # Each of the following have a symbol key for the table alias, with the following values: # :reciprocals - the reciprocal instance variable to use for this association # :reflections - AssociationReflection instance related to this association # :requirements - array of requirements for this association clone(:eager_graph=>{:requirements=>{}, :master=>alias_symbol(first_source), :reflections=>{}, :reciprocals=>{}, :cartesian_product_number=>0}) end ds.eager_graph_associations(ds, model, ds.opts[:eager_graph][:master], [], *associations) end # Do not attempt to split the result set into associations, # just return results as simple objects. This is useful if you # want to use eager_graph as a shortcut to have all of the joins # and aliasing set up, but want to do something else with the dataset. def ungraphed super.clone(:eager_graph=>nil) end protected # Call graph on the association with the correct arguments, # update the eager_graph data structure, and recurse into # eager_graph_associations if there are any passed in associations # (which would be dependencies of the current association) # # Arguments: # ds :: Current dataset # model :: Current Model # ta :: table_alias used for the parent association # requirements :: an array, used as a stack for requirements # r :: association reflection for the current association, or an SQL::AliasedExpression # with the reflection as the expression and the alias base as the aliaz. # *associations :: any associations dependent on this one def eager_graph_association(ds, model, ta, requirements, r, *associations) if r.is_a?(SQL::AliasedExpression) alias_base = r.aliaz r = r.expression else alias_base = r[:graph_alias_base] end assoc_name = r[:name] assoc_table_alias = ds.unused_table_alias(alias_base) loader = r[:eager_grapher] if !associations.empty? if associations.first.respond_to?(:call) callback = associations.first associations = {} elsif associations.length == 1 && (assocs = associations.first).is_a?(Hash) && assocs.length == 1 && (pr_assoc = assocs.to_a.first) && pr_assoc.first.respond_to?(:call) callback, assoc = pr_assoc associations = assoc.is_a?(Array) ? assoc : [assoc] end end ds = if loader.arity == 1 loader.call(:self=>ds, :table_alias=>assoc_table_alias, :implicit_qualifier=>ta, :callback=>callback) else loader.call(ds, assoc_table_alias, ta) end ds = ds.order_more(*qualified_expression(r[:order], assoc_table_alias)) if r[:order] and r[:order_eager_graph] eager_graph = ds.opts[:eager_graph] eager_graph[:requirements][assoc_table_alias] = requirements.dup eager_graph[:reflections][assoc_table_alias] = r eager_graph[:cartesian_product_number] += r[:cartesian_product_number] || 2 ds = ds.eager_graph_associations(ds, r.associated_class, assoc_table_alias, requirements + [assoc_table_alias], *associations) unless associations.empty? ds end # Check the associations are valid for the given model. # Call eager_graph_association on each association. # # Arguments: # ds :: Current dataset # model :: Current Model # ta :: table_alias used for the parent association # requirements :: an array, used as a stack for requirements # *associations :: the associations to add to the graph def eager_graph_associations(ds, model, ta, requirements, *associations) return ds if associations.empty? associations.flatten.each do |association| ds = case association when Symbol, SQL::AliasedExpression ds.eager_graph_association(ds, model, ta, requirements, eager_graph_check_association(model, association)) when Hash association.each do |assoc, assoc_assocs| ds = ds.eager_graph_association(ds, model, ta, requirements, eager_graph_check_association(model, assoc), assoc_assocs) end ds else raise(Sequel::Error, 'Associations must be in the form of a symbol or hash') end end ds end # Replace the array of plain hashes with an array of model objects will all eager_graphed # associations set in the associations cache for each object. def eager_graph_build_associations(hashes) hashes.replace(EagerGraphLoader.new(self).load(hashes)) end private # Return an expression for filtering by the given association reflection and associated object. def association_filter_expression(op, ref, obj) meth = :"#{ref[:type]}_association_filter_expression" send(meth, op, ref, obj) if respond_to?(meth, true) end # Handle inversion for association filters by returning an inverted expression, # plus also handling cases where the referenced columns are NULL. def association_filter_handle_inversion(op, exp, cols) if op == :'!=' || op == :'NOT IN' if exp == SQL::Constants::FALSE ~exp else ~exp | Sequel::SQL::BooleanExpression.from_value_pairs(cols.zip([]), :OR) end else exp end end # Return an expression for making sure that the given keys match the value of # the given methods for either the single object given or for any of the objects # given if +obj+ is an array. def association_filter_key_expression(keys, meths, obj) vals = if obj.is_a?(Sequel::Dataset) {(keys.length == 1 ? keys.first : keys)=>obj.select(*meths).exclude(Sequel::SQL::BooleanExpression.from_value_pairs(meths.zip([]), :OR))} else vals = Array(obj).reject{|o| !meths.all?{|m| o.send(m)}} return SQL::Constants::FALSE if vals.empty? if obj.is_a?(Array) if keys.length == 1 meth = meths.first {keys.first=>vals.map{|o| o.send(meth)}} else {keys=>vals.map{|o| meths.map{|m| o.send(m)}}} end else keys.zip(meths.map{|k| obj.send(k)}) end end SQL::BooleanExpression.from_value_pairs(vals) end # Make sure the association is valid for this model, and return the related AssociationReflection. def check_association(model, association) raise(Sequel::UndefinedAssociation, "Invalid association #{association} for #{model.name}") unless reflection = model.association_reflection(association) raise(Sequel::Error, "Eager loading is not allowed for #{model.name} association #{association}") if reflection[:allow_eager] == false reflection end # Allow associations that are eagerly graphed to be specified as an SQL::AliasedExpression, for # per-call determining of the alias base. def eager_graph_check_association(model, association) if association.is_a?(SQL::AliasedExpression) SQL::AliasedExpression.new(check_association(model, association.expression), association.aliaz) else check_association(model, association) end end # Eagerly load all specified associations def eager_load(a, eager_assoc=@opts[:eager]) return if a.empty? # Key is foreign/primary key name symbol # Value is hash with keys being foreign/primary key values (generally integers) # and values being an array of current model objects with that # specific foreign/primary key key_hash = {} # Reflections for all associations to eager load reflections = eager_assoc.keys.collect{|assoc| model.association_reflection(assoc) || (raise Sequel::UndefinedAssociation, "Model: #{self}, Association: #{assoc}")} # Populate the key_hash entry for each association being eagerly loaded reflections.each do |r| if key = r.eager_loader_key # key_hash for this key has already been populated, # skip populating again so that duplicate values # aren't added. unless id_map = key_hash[key] id_map = key_hash[key] = Hash.new{|h,k| h[k] = []} # Supporting both single (Symbol) and composite (Array) keys. a.each do |rec| case key when Array if (k = key.map{|k2| rec.send(k2)}) && k.all? id_map[k] << rec end when Symbol if k = rec.send(key) id_map[k] << rec end else raise Error, "unhandled eager_loader_key #{key.inspect} for association #{r[:name]}" end end end else id_map = nil end loader = r[:eager_loader] associations = eager_assoc[r[:name]] if associations.respond_to?(:call) eager_block = associations associations = {} elsif associations.is_a?(Hash) && associations.length == 1 && (pr_assoc = associations.to_a.first) && pr_assoc.first.respond_to?(:call) eager_block, associations = pr_assoc end if loader.arity == 1 loader.call(:key_hash=>key_hash, :rows=>a, :associations=>associations, :self=>self, :eager_block=>eager_block, :id_map=>id_map) else loader.call(key_hash, a, associations) end a.each{|object| object.send(:run_association_callbacks, r, :after_load, object.associations[r[:name]])} unless r[:after_load].empty? end end # Return plain hashes instead of calling the row_proc if eager_graph is being used. def graph_each(&block) @opts[:eager_graph] ? fetch_rows(select_sql, &block) : super end # Return a subquery expression for filering by a many_to_many association def many_to_many_association_filter_expression(op, ref, obj) lpks, lks, rks = ref.values_at(:left_primary_key_columns, :left_keys, :right_keys) jt = ref.join_table_alias lpks = lpks.first if lpks.length == 1 lpks = ref.qualify(model.table_name, lpks) meths = if obj.is_a?(Sequel::Dataset) ref.qualify(obj.model.table_name, ref.right_primary_keys) else ref.right_primary_key_methods end exp = association_filter_key_expression(ref.qualify(jt, rks), meths, obj) if exp == SQL::Constants::FALSE association_filter_handle_inversion(op, exp, Array(lpks)) else association_filter_handle_inversion(op, SQL::BooleanExpression.from_value_pairs(lpks=>model.db.from(ref[:join_table]).select(*ref.qualify(jt, lks)).where(exp).exclude(SQL::BooleanExpression.from_value_pairs(ref.qualify(jt, lks).zip([]), :OR))), Array(lpks)) end end # Return a simple equality expression for filering by a many_to_one association def many_to_one_association_filter_expression(op, ref, obj) keys = ref.qualify(model.table_name, ref[:key_columns]) meths = if obj.is_a?(Sequel::Dataset) ref.qualify(obj.model.table_name, ref.primary_keys) else ref.primary_key_methods end association_filter_handle_inversion(op, association_filter_key_expression(keys, meths, obj), keys) end # Return a simple equality expression for filering by a one_to_* association def one_to_many_association_filter_expression(op, ref, obj) keys = ref.qualify(model.table_name, ref[:primary_key_columns]) meths = if obj.is_a?(Sequel::Dataset) ref.qualify(obj.model.table_name, ref[:keys]) else ref[:key_methods] end association_filter_handle_inversion(op, association_filter_key_expression(keys, meths, obj), keys) end alias one_to_one_association_filter_expression one_to_many_association_filter_expression # Build associations from the graph if #eager_graph was used, # and/or load other associations if #eager was used. def post_load(all_records) eager_graph_build_associations(all_records) if @opts[:eager_graph] eager_load(all_records) if @opts[:eager] super end end # This class is the internal implementation of eager_graph. It is responsible for taking an array of plain # hashes and returning an array of model objects with all eager_graphed associations already set in the # association cache. class EagerGraphLoader # Hash with table alias symbol keys and after_load hook values attr_reader :after_load_map # Hash with table alias symbol keys and association name values attr_reader :alias_map # Hash with table alias symbol keys and subhash values mapping column_alias symbols to the # symbol of the real name of the column attr_reader :column_maps # Recursive hash with table alias symbol keys mapping to hashes with dependent table alias symbol keys. attr_reader :dependency_map # Hash with table alias symbol keys and [limit, offset] values attr_reader :limit_map # Hash with table alias symbol keys and callable values used to create model instances # The table alias symbol for the primary model attr_reader :master # Hash with table alias symbol keys and primary key symbol values (or arrays of primary key symbols for # composite key tables) attr_reader :primary_keys # Hash with table alias symbol keys and reciprocal association symbol values, # used for setting reciprocals for one_to_many associations. attr_reader :reciprocal_map # Hash with table alias symbol keys and subhash values mapping primary key symbols (or array of symbols) # to model instances. Used so that only a single model instance is created for each object. attr_reader :records_map # Hash with table alias symbol keys and AssociationReflection values attr_reader :reflection_map # Hash with table alias symbol keys and callable values used to create model instances attr_reader :row_procs # Hash with table alias symbol keys and true/false values, where true means the # association represented by the table alias uses an array of values instead of # a single value (i.e. true => *_many, false => *_to_one). attr_reader :type_map # Initialize all of the data structures used during loading. def initialize(dataset) opts = dataset.opts eager_graph = opts[:eager_graph] @master = eager_graph[:master] requirements = eager_graph[:requirements] reflection_map = @reflection_map = eager_graph[:reflections] reciprocal_map = @reciprocal_map = eager_graph[:reciprocals] @unique = eager_graph[:cartesian_product_number] > 1 alias_map = @alias_map = {} type_map = @type_map = {} after_load_map = @after_load_map = {} limit_map = @limit_map = {} reflection_map.each do |k, v| alias_map[k] = v[:name] type_map[k] = v.returns_array? after_load_map[k] = v[:after_load] unless v[:after_load].empty? limit_map[k] = v.limit_and_offset if v[:limit] end # Make dependency map hash out of requirements array for each association. # This builds a tree of dependencies that will be used for recursion # to ensure that all parts of the object graph are loaded into the # appropriate subordinate association. @dependency_map = {} # Sort the associations by requirements length, so that # requirements are added to the dependency hash before their # dependencies. requirements.sort_by{|a| a[1].length}.each do |ta, deps| if deps.empty? dependency_map[ta] = {} else deps = deps.dup hash = dependency_map[deps.shift] deps.each do |dep| hash = hash[dep] end hash[ta] = {} end end # This mapping is used to make sure that duplicate entries in the # result set are mapped to a single record. For example, using a # single one_to_many association with 10 associated records, # the main object column values appear in the object graph 10 times. # We map by primary key, if available, or by the object's entire values, # if not. The mapping must be per table, so create sub maps for each table # alias. records_map = {@master=>{}} alias_map.keys.each{|ta| records_map[ta] = {}} @records_map = records_map datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?} column_aliases = opts[:graph_aliases] || opts[:graph][:column_aliases] primary_keys = {} column_maps = {} models = {} row_procs = {} datasets.each do |ta, ds| models[ta] = ds.model primary_keys[ta] = [] column_maps[ta] = {} row_procs[ta] = ds.row_proc end column_aliases.each do |col_alias, tc| ta, column = tc column_maps[ta][col_alias] = column end column_maps.each do |ta, h| pk = models[ta].primary_key if pk.is_a?(Array) primary_keys[ta] = [] h.select{|ca, c| primary_keys[ta] << ca if pk.include?(c)} else h.select{|ca, c| primary_keys[ta] = ca if pk == c} end end @column_maps = column_maps @primary_keys = primary_keys @row_procs = row_procs # For performance, create two special maps for the master table, # so you can skip a hash lookup. @master_column_map = column_maps[master] @master_primary_keys = primary_keys[master] # Add a special hash mapping table alias symbols to 5 element arrays that just # contain the data in other data structures for that table alias. This is # used for performance, to get all values in one hash lookup instead of # separate hash lookups for each data structure. ta_map = {} alias_map.keys.each do |ta| ta_map[ta] = [records_map[ta], row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]] end @ta_map = ta_map end # Return an array of primary model instances with the associations cache prepopulated # for all model objects (both primary and associated). def load(hashes) master = master() # Assign to local variables for speed increase rp = row_procs[master] rm = records_map[master] dm = dependency_map # This will hold the final record set that we will be replacing the object graph with. records = [] hashes.each do |h| unless key = master_pk(h) key = hkey(master_hfor(h)) end unless primary_record = rm[key] primary_record = rm[key] = rp.call(master_hfor(h)) # Only add it to the list of records to return if it is a new record records.push(primary_record) end # Build all associations for the current object and it's dependencies _load(dm, primary_record, h) end # Remove duplicate records from all associations if this graph could possibly be a cartesian product # Run after_load procs if there are any post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty? records end private # Recursive method that creates associated model objects and associates them to the current model object. def _load(dependency_map, current, h) dependency_map.each do |ta, deps| unless key = pk(ta, h) ta_h = hfor(ta, h) unless ta_h.values.any? assoc_name = alias_map[ta] unless (assoc = current.associations).has_key?(assoc_name) assoc[assoc_name] = type_map[ta] ? [] : nil end next end key = hkey(ta_h) end rm, rp, assoc_name, tm, rcm = @ta_map[ta] unless rec = rm[key] rec = rm[key] = rp.call(hfor(ta, h)) end if tm unless (assoc = current.associations).has_key?(assoc_name) assoc[assoc_name] = [] end assoc[assoc_name].push(rec) rec.associations[rcm] = current if rcm else current.associations[assoc_name] ||= rec end # Recurse into dependencies of the current object _load(deps, rec, h) unless deps.empty? end end # Return the subhash for the specific table alias +ta+ by parsing the values out of the main hash +h+ def hfor(ta, h) out = {} @column_maps[ta].each{|ca, c| out[c] = h[ca]} out end # Return a suitable hash key for any subhash +h+, which is an array of values by column order. # This is only used if the primary key cannot be used. def hkey(h) h.sort_by{|x| x[0].to_s} end # Return the subhash for the master table by parsing the values out of the main hash +h+ def master_hfor(h) out = {} @master_column_map.each{|ca, c| out[c] = h[ca]} out end # Return a primary key value for the master table by parsing it out of the main hash +h+. def master_pk(h) x = @master_primary_keys if x.is_a?(Array) unless x == [] x = x.map{|ca| h[ca]} x if x.all? end else h[x] end end # Return a primary key value for the given table alias by parsing it out of the main hash +h+. def pk(ta, h) x = primary_keys[ta] if x.is_a?(Array) unless x == [] x = x.map{|ca| h[ca]} x if x.all? end else h[x] end end # If the result set is the result of a cartesian product, then it is possible that # there are multiple records for each association when there should only be one. # In that case, for each object in all associations loaded via +eager_graph+, run # uniq! on the association to make sure no duplicate records show up. # Note that this can cause legitimate duplicate records to be removed. def post_process(records, dependency_map) records.each do |record| dependency_map.each do |ta, deps| assoc_name = alias_map[ta] list = record.send(assoc_name) rec_list = if type_map[ta] list.uniq! if lo = limit_map[ta] limit, offset = lo list.replace(list[offset||0, limit]) end list elsif list [list] else [] end record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta] post_process(rec_list, deps) if !rec_list.empty? && !deps.empty? end end end end end end end