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 _dataset association method def _dataset_method :"_#{self[:name]}_dataset" 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 self[:class] ||= constantize(self[:class_name]) end # Name symbol for the dataset association method def dataset_method :"#{self[:name]}_dataset" end # Name symbol for the _helper internal association method def dataset_helper_method :"_#{self[:name]}_dataset_helper" end # Whether the dataset needs a primary key to function, true by default. def dataset_need_primary_key? true 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 # 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 # Whether the associated object needs a primary key to be added/removed, # false by default. def need_associated_primary_key? false 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 return self[:reciprocal] if include?(:reciprocal) r_type = reciprocal_type key = self[:key] associated_class.all_association_reflections.each do |assoc_reflect| if assoc_reflect[:type] == r_type && assoc_reflect[:key] == key && assoc_reflect.associated_class == self[:model] return self[:reciprocal] = assoc_reflect[:name] end end self[:reciprocal] = nil 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 # Name symbol for the remove_ association method def remove_method :"remove_#{singularize(self[:name])}" 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, nil by default. 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 end class ManyToOneAssociationReflection < AssociationReflection ASSOCIATION_TYPES[:many_to_one] = self # 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 # The key to use for the key hash when eager loading def eager_loader_key self[:key] end # The column in the associated table that the key in the current table references. def primary_key self[:primary_key] ||= associated_class.primary_key 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 private # The reciprocal type of a many_to_one association is a one_to_many association. def reciprocal_type :one_to_many end end class OneToManyAssociationReflection < AssociationReflection ASSOCIATION_TYPES[:one_to_many] = self # 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 column in the current table that the key in the associated table references. def primary_key self[:primary_key] ||= self[:model].primary_key end alias eager_loader_key primary_key # One to many associations set the reciprocal to self when loading associated records. def set_reciprocal_to_self? true 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 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 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 # The table containing the column to use for the associated key when eagerly loading def associated_key_table self[:join_table] end # The default associated key alias def default_associated_key_alias :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 key to use for the key hash when eager loading def eager_loader_key self[:left_primary_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 # 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 return self[:reciprocal] if include?(:reciprocal) left_key = self[:left_key] right_key = self[:right_key] join_table = self[:join_table] associated_class.all_association_reflections.each do |assoc_reflect| if assoc_reflect[:type] == :many_to_many && assoc_reflect[:left_key] == right_key && assoc_reflect[:right_key] == left_key && assoc_reflect[:join_table] == join_table && assoc_reflect.associated_class == self[:model] return self[:reciprocal] = assoc_reflect[:name] end end self[:reciprocal] = nil end # The primary key column to use in the associated table. def right_primary_key self[:right_primary_key] ||= associated_class.primary_key end # The columns to select when loading the association, associated_class.table_name.* by default. def select return self[:select] if include?(:select) self[:select] ||= Sequel::SQL::ColumnAll.new(associated_class.table_name) 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 # 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. # * 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, # there are private instance methods created that a prepended with an # underscore (e.g. _add_milestone). 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. # # Association definitions are also reflected by the class, e.g.: # # Project.associations # => [:portfolio, :milestones] # Project.association_reflection(:portfolio) # => {:type => :many_to_one, :name => :portfolio, :class_name => "Portfolio"} module ClassMethods # All association reflections defined for this model (default: none). attr_reader :association_reflections # Array of all association reflections for this model class def all_association_reflections association_reflections.values 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. # * :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. # # A one to one relationship can be set up with a many_to_one association # on the table with the foreign key, and a one_to_many association with the # :one_to_one option specified on the table without the foreign key. The # two associations will operate similarly, except that the many_to_one # association setter doesn't update the database until you call save manually. # Also, in most cases you need to specify the plural association name when using # one_to_many with the :one_to_one option. # # 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. Not called # when eager loading via eager_graph, but called when eager loading via eager. # - :after_remove - Symbol, Proc, or array of both/either specifying a callback to call # after an item is removed from the association. # - :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. # - :cartesian_product_number - he 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 associations, # 1 for *_to_many associations). # - :class - The associated class or its name. If not # given, uses the association's name, which is camelized (and # singularized unless the type is :many_to_one) # - :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 filter. # - :dataset - A proc that is instance_evaled to get the base dataset # to use for the _dataset method (before the other options are applied). # - :eager - The associations to eagerly load via #eager when loading the associated object(s). # For many_to_one associations, this is ignored unless this association is # being eagerly loaded, as it doesn't save queries unless multiple objects # can be loaded at once. # - :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). # For many_to_one associations, this is ignored unless this association is # being eagerly loaded, as it doesn't save queries unless multiple objects # can be loaded at once. # - :eager_grapher - A proc to use to implement eager loading via eager graph, overriding the default. # Takes three arguments, 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), # Should return a copy of the dataset with the association graphed into it. # - :eager_loader - A proc to use to implement eager loading, overriding the default. Takes three arguments, # a key hash (used solely to enhance performance), an array of records, # and a hash of dependent associations. The associated records should # be queried from the database and the associations cache for each # record should be populated for this to work correctly. # - :extend - A module or array of modules to extend the dataset with. # - :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 all two pairs. If not # specified, the :conditions option is used if it is a hash or array of all two pairs. # - :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 arguments for the value to specify a limit and an offset. # - :order - the column(s) by which to order the association dataset. Can be a # singular column or an array. # - :order_eager_graph - Whether to add the order to the 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_many with :one_to_one), # or add_/remove_/remove_all_ methods (for one_to_many, many_to_many) # - :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 attributes 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. # * :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". # - :primary_key - column in the associated table that :key option references, as a symbol. # Defaults to the primary key of the associated table. # * :one_to_many: # - :key - foreign key in associated model's table that references # current model's primary key, as a symbol. Defaults to # :"#{self.name.underscore}_id". # - :one_to_one: Create a getter and setter similar to those of many_to_one # associations. The getter returns a singular matching record, or raises an # error if multiple records match. The setter updates the record given and removes # associations with all other records. When this option is used, the other # association methods usually added are either removed or made private, # so using this is similar to using many_to_one, in terms of the methods # it adds, the main difference is that the foreign key is in the associated # table instead of the current table. Note that using this option still requires # you to use a plural name when creating and using the association (e.g. for reflections, eager loading, etc.). # - :primary_key - column in the current table that :key option references, as a symbol. # Defaults to primary key of the current table. # * :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 all two pairs. # - :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 '_'. # - :left_key - foreign key in join table that points to current model's # primary key, as a symbol. Defaults to :"#{self.name.underscore}_id". # - :left_primary_key - column in current table that :left_key points to, as a symbol. # Defaults to primary key of current table. # - :right_key - foreign key in join table that points to associated # model's primary key, as a symbol. Defaults to Defaults to :"#{name.to_s.singularize}_id". # - :right_primary_key - column in associated table that :right_key points to, as a symbol. # Defaults to primary key of the associated table. # - :uniq - Adds a after_load callback that makes the array of objects unique. def associate(type, name, opts = {}, &block) raise(Error, 'invalid association type') unless assoc_class = ASSOCIATION_TYPES[type] raise(Error, 'Model.associate name argument must be a symbol') unless Symbol === name # merge early so we don't modify opts orig_opts = opts.dup orig_opts = association_reflection(opts[:clone])[:orig_opts].merge(orig_opts) if opts[:clone] opts = orig_opts.merge(:type => type, :name => name, :cache => true, :model => self) opts[:block] = block if block opts = assoc_class.new.merge!(opts) opts[:eager_block] = block unless opts.include?(:eager_block) opts[:graph_join_type] ||= :left_outer opts[:order_eager_graph] = true unless opts.include?(:order_eager_graph) conds = opts[:conditions] opts[:graph_conditions] = conds if !opts.include?(:graph_conditions) and Sequel.condition_specifier?(conds) opts[:graph_conditions] = opts[:graph_conditions] ? opts[: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, :extend].each do |cb_type| opts[cb_type] = Array(opts[cb_type]) end # find class case opts[:class] when String, Symbol # Delete :class to allow late binding opts[:class_name] ||= opts.delete(:class).to_s when Class opts[:class_name] ||= opts[:class].name end opts[:class_name] ||= ((self.name || '').split("::")[0..-2] + [camelize(opts.returns_array? ? singularize(name) : name)]).join('::') 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. Options: def eager_loading_dataset(opts, ds, select, associations) ds = ds.select(*select) if select if c = opts[:conditions] ds = (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.filter(*c) : ds.filter(c) end ds = ds.order(*opts[:order]) if opts[:order] ds = ds.eager(opts[:eager]) if opts[:eager] if opts[:eager_graph] ds = ds.eager_graph(opts[:eager_graph]) ds = ds.add_graph_aliases(opts.associated_key_alias=>[opts.associated_class.table_name, opts.associated_key_alias, SQL::QualifiedIdentifier.new(opts.associated_key_table, opts.associated_key_column)]) if opts.eager_loading_use_associated_key? else ds.select_more(SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(opts.associated_key_table, opts.associated_key_column), opts.associated_key_alias)) if opts.eager_loading_use_associated_key? end ds = ds.eager(associations) unless Array(associations).empty? ds = opts[:eager_block].call(ds) if opts[:eager_block] ds end # Copy the association reflections to the subclass def inherited(subclass) super subclass.instance_variable_set(:@association_reflections, @association_reflections.dup) end # Shortcut for adding a many_to_many association, see associate def many_to_many(*args, &block) associate(:many_to_many, *args, &block) end # Shortcut for adding a many_to_one association, see associate def many_to_one(*args, &block) associate(:many_to_one, *args, &block) end # Shortcut for adding a one_to_many association, see associate def one_to_many(*args, &block) associate(:one_to_many, *args, &block) end private # 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, &block) overridable_methods_module.module_eval{define_method(name, &block)} end # Add a private method to the module included in the class. def association_module_private_def(name, &block) association_module_def(name, &block) overridable_methods_module.send(:private, name) end # Add the add_ instance method def def_add_method(opts) association_module_def(opts.add_method){|o,*args| add_associated_object(opts, o, *args)} end # Adds methods related to the association's dataset to the module included in the class. def def_association_dataset_methods(opts) # If a block is given, define a helper method for it, because it takes # an argument. This is unnecessary in Ruby 1.9, as that has instance_exec. association_module_private_def(opts.dataset_helper_method, &opts[:block]) if opts[:block] association_module_private_def(opts._dataset_method, &opts[:dataset]) association_module_def(opts.dataset_method){_dataset(opts)} def_association_method(opts) end # Adds method for retrieving the associated objects to the module included in the class. def def_association_method(opts) association_module_def(opts.association_method){|*reload| load_associated_objects(opts, reload[0])} end # Adds many_to_many association instance methods def def_many_to_many(opts) name = opts[:name] model = self left = (opts[:left_key] ||= opts.default_left_key) right = (opts[:right_key] ||= opts.default_right_key) left_pk = (opts[:left_primary_key] ||= self.primary_key) 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[:graph_join_table_conditions] ? opts[: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_class.inner_join(join_table, [[right, opts.right_primary_key], [left, send(left_pk)]])} database = db opts[:eager_loader] ||= proc do |key_hash, records, associations| h = key_hash[left_pk] records.each{|object| object.associations[name] = []} model.eager_loading_dataset(opts, opts.associated_class.inner_join(join_table, [[right, opts.right_primary_key], [left, h.keys]]), Array(opts.select), associations).all do |assoc_record| next unless objects = h[assoc_record.values.delete(left_key_alias)] objects.each{|object| object.associations[name].push(assoc_record)} 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 |ds, assoc_alias, table_alias| ds = ds.graph(join_table, use_jt_only_conditions ? jt_only_conditions : [[left, left_pk]] + graph_jt_conds, :select=>false, :table_alias=>ds.send(:eager_unique_table_alias, ds, join_table), :join_type=>jt_join_type, :implicit_qualifier=>table_alias, &jt_graph_block) ds.graph(opts.associated_class, use_only_conditions ? only_conditions : [[opts.right_primary_key, right]] + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, &graph_block) end def_association_dataset_methods(opts) return if opts[:read_only] association_module_private_def(opts._add_method) do |o| database.dataset.from(join_table).insert(left=>send(left_pk), right=>o.send(opts.right_primary_key)) end association_module_private_def(opts._remove_method) do |o| database.dataset.from(join_table).filter([[left, send(left_pk)], [right, o.send(opts.right_primary_key)]]).delete end association_module_private_def(opts._remove_all_method) do database.dataset.from(join_table).filter(left=>send(left_pk)).delete end def_add_method(opts) def_remove_methods(opts) end # Adds many_to_one association instance methods def def_many_to_one(opts) name = opts[:name] model = self opts[:key] = opts.default_key unless opts.include?(:key) key = opts[:key] opts[:cartesian_product_number] ||= 0 opts[:dataset] ||= proc do klass = opts.associated_class klass.filter(SQL::QualifiedIdentifier.new(klass.table_name, opts.primary_key)=>send(key)) end opts[:eager_loader] ||= proc do |key_hash, records, associations| h = key_hash[key] keys = h.keys # Default the cached association to nil, so any object that doesn't have it # populated will have cached the negative lookup. records.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.filter(SQL::QualifiedIdentifier.new(klass.table_name, opts.primary_key)=>keys), opts.select, associations).all do |assoc_record| next unless objects = h[assoc_record.send(opts.primary_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] opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias| ds.graph(opts.associated_class, use_only_conditions ? only_conditions : [[opts.primary_key, key]] + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, :implicit_qualifier=>table_alias, &graph_block) end def_association_dataset_methods(opts) return if opts[:read_only] association_module_private_def(opts._setter_method){|o| send(:"#{key}=", (o.send(opts.primary_key) if o))} association_module_def(opts.setter_method){|o| set_associated_object(opts, o)} end # Adds one_to_many association instance methods def def_one_to_many(opts) name = opts[:name] model = self key = (opts[:key] ||= opts.default_key) primary_key = (opts[:primary_key] ||= self.primary_key) opts[:dataset] ||= proc do klass = opts.associated_class klass.filter(SQL::QualifiedIdentifier.new(klass.table_name, key) => send(primary_key)) end opts[:eager_loader] ||= proc do |key_hash, records, associations| h = key_hash[primary_key] records.each{|object| object.associations[name] = []} reciprocal = opts.reciprocal klass = opts.associated_class model.eager_loading_dataset(opts, klass.filter(SQL::QualifiedIdentifier.new(klass.table_name, key)=>h.keys), opts.select, associations).all do |assoc_record| next unless objects = h[assoc_record[key]] objects.each do |object| object.associations[name].push(assoc_record) assoc_record.associations[reciprocal] = object if reciprocal 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] opts[:cartesian_product_number] ||= 1 graph_block = opts[:graph_block] opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias| ds = ds.graph(opts.associated_class, use_only_conditions ? only_conditions : [[key, primary_key]] + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, :implicit_qualifier=>table_alias, &graph_block) # We only load reciprocals for one_to_many associations, as other reciprocals don't make sense ds.opts[:eager_graph][:reciprocals][assoc_alias] = opts.reciprocal ds end def_association_dataset_methods(opts) unless opts[:read_only] association_module_private_def(opts._add_method) do |o| o.send(:"#{key}=", send(primary_key)) o.save || raise(Sequel::Error, "invalid associated object, cannot save") end def_add_method(opts) unless opts[:one_to_one] association_module_private_def(opts._remove_method) do |o| o.send(:"#{key}=", nil) o.save || raise(Sequel::Error, "invalid associated object, cannot save") end association_module_private_def(opts._remove_all_method) do opts.associated_class.filter(key=>send(primary_key)).update(key=>nil) end def_remove_methods(opts) end end if opts[:one_to_one] overridable_methods_module.send(:private, opts.association_method, opts.dataset_method) n = singularize(name).to_sym raise(Sequel::Error, "one_to_many association names should still be plural even when using the :one_to_one option") if n == name association_module_def(n) do |*o| objs = send(name, *o) raise(Sequel::Error, "multiple values found for a one-to-one relationship") if objs.length > 1 objs.first end unless opts[:read_only] overridable_methods_module.send(:private, opts.add_method) association_module_def(:"#{n}=") do |o| klass = opts.associated_class update_database = lambda do send(opts.add_method, o) klass.filter(Sequel::SQL::BooleanExpression.new(:AND, {key=>send(primary_key)}, SQL::BooleanExpression.new(:'!=', klass.primary_key, o.pk))).update(key=>nil) end use_transactions ? db.transaction(opts){update_database.call} : update_database.call end end end end # Add the remove_ and remove_all instance methods def def_remove_methods(opts) association_module_def(opts.remove_method){|o,*args| remove_associated_object(opts, o, *args)} association_module_def(opts.remove_all_method){|*args| remove_all_associated_objects(opts, *args)} end end # Private instance methods used to implement the associations support. module InstanceMethods # Used internally by the associations code, like pk but doesn't raise # an Error if the model has no primary key. def pk_or_nil key = primary_key key.is_a?(Array) ? key.map{|k| @values[k]} : @values[key] end private # Backbone behind association dataset methods def _dataset(opts) raise(Sequel::Error, "model object #{model} does not have a primary key") if opts.dataset_need_primary_key? && !pk ds = send(opts._dataset_method) ds.extend(AssociationDatasetMethods) ds.model_object = self ds.association_reflection = opts opts[:extend].each{|m| ds.extend(m)} ds = ds.select(*opts.select) if opts.select if c = opts[:conditions] ds = (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.filter(*c) : ds.filter(c) end ds = ds.order(*opts[:order]) if opts[:order] ds = ds.limit(*opts[:limit]) if opts[:limit] ds = ds.eager(*opts[:eager]) if opts[:eager] ds = ds.eager_graph(opts[:eager_graph]) if opts[:eager_graph] && opts.eager_graph_lazy_dataset? ds = send(opts.dataset_helper_method, ds) if opts[:block] ds end # Return the associated objects from the dataset, without callbacks, reciprocals, and caching. def _load_associated_objects(opts) if opts.returns_array? send(opts.dataset_method).all else if !opts[:key] send(opts.dataset_method).all.first elsif send(opts[:key]) send(opts.dataset_method).first end end end # Add the given associated object to the given association def add_associated_object(opts, o, *args) raise(Sequel::Error, "model object #{model} does not have a primary key") unless pk raise(Sequel::Error, "associated object #{o.model} does not have a primary key") if opts.need_associated_primary_key? && !o.pk return if run_association_callbacks(opts, :before_add, o) == false send(opts._add_method, o, *args) associations[opts[:name]].push(o) if associations.include?(opts[:name]) 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 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 # Load the associated objects using the dataset, handling callbacks, reciprocals, and caching. def load_associated_objects(opts, reload=false) name = opts[:name] if associations.include?(name) and !reload associations[name] else objs = _load_associated_objects(opts) run_association_callbacks(opts, :after_load, objs) objs.each{|o| add_reciprocal_object(opts, o)} if opts.set_reciprocal_to_self? associations[name] = objs end end # Remove all associated objects from the given association def remove_all_associated_objects(opts, *args) raise(Sequel::Error, "model object #{model} 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) raise(Sequel::Error, "model object #{model} does not have a primary key") unless pk raise(Sequel::Error, "associated object #{o.model} 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 # 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].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 record: 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 association def set_associated_object(opts, o) raise(Sequel::Error, "model object #{model} does not have a primary key") if o && !o.pk old_val = send(opts.association_method) return o if old_val == o return if old_val and run_association_callbacks(opts, :before_remove, old_val) == false return if o and run_association_callbacks(opts, :before_add, o) == false send(opts._setter_method, o) associations[opts[:name]] = o remove_reciprocal_object(opts, old_val) if old_val if o add_reciprocal_object(opts, o) run_association_callbacks(opts, :after_add, o) end run_association_callbacks(opts, :after_remove, old_val) if old_val 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 based on columns in associated tables. #eager_graph loads # all records in one query. Using #eager_graph you can filter based on columns in associated # tables. However, #eager_graph can be slower than #eager, especially if multiple # *_to_many associations are joined. # # You can cascade the eager loading (loading associations' associations) # 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 module DatasetMethods # Add the #eager! and #eager_graph! mutation methods to the dataset. def self.extended(obj) obj.def_mutation_method(:eager, :eager_graph) 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 *_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. Eager also works # on a limited dataset, but does not use any :limit options for associations. # 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 based on columns in associated tables. # # This method builds an object graph using Dataset#graph. Then it uses the graph # to build the associations, and finally replaces the graph with a simple array # of model objects. # # Be very careful when using this with multiple *_to_many associations, as you can # create large cartesian products. If you must graph multiple *_to_many associations, # make sure your filters are specific 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 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, you will get a normal graphed result back (a hash with model object values). def eager_graph(*associations) table_name = model.table_name ds = if @opts[:eager_graph] self 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 # :requirements - array of requirements for this association # :alias_association_type_map - the type of association for this association # :alias_association_name_map - the name of the association for this association clone(:eager_graph=>{:requirements=>{}, :master=>model.table_name, :alias_association_type_map=>{}, :alias_association_name_map=>{}, :reciprocals=>{}, :cartesian_product_number=>0}) end ds.eager_graph_associations(ds, model, table_name, [], *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 # * *associations - any associations dependent on this one def eager_graph_association(ds, model, ta, requirements, r, *associations) klass = r.associated_class assoc_name = r[:name] assoc_table_alias = ds.eager_unique_table_alias(ds, assoc_name) ds = r[:eager_grapher].call(ds, assoc_table_alias, ta) ds = ds.order_more(*Array(r[:order]).map{|c| eager_graph_qualify_order(assoc_table_alias, c)}) if r[:order] and r[:order_eager_graph] eager_graph = ds.opts[:eager_graph] eager_graph[:requirements][assoc_table_alias] = requirements.dup eager_graph[:alias_association_name_map][assoc_table_alias] = assoc_name eager_graph[:alias_association_type_map][assoc_table_alias] = r.returns_array? 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 ds.eager_graph_association(ds, model, ta, requirements, check_association(model, association)) when Hash association.each do |assoc, assoc_assocs| ds = ds.eager_graph_association(ds, model, ta, requirements, 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 # Build associations out of the array of returned object graphs. def eager_graph_build_associations(record_graphs) eager_graph = @opts[:eager_graph] master = eager_graph[:master] requirements = eager_graph[:requirements] alias_map = eager_graph[:alias_association_name_map] type_map = eager_graph[:alias_association_type_map] reciprocal_map = eager_graph[:reciprocals] # 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 will 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] = {}} # This will hold the final record set that we will be replacing the object graph with. records = [] record_graphs.each do |record_graph| primary_record = record_graph[master] key = primary_record.pk_or_nil || primary_record.values.sort_by{|x| x[0].to_s} if cached_pr = records_map[master][key] primary_record = cached_pr else records_map[master][key] = primary_record # 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 eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, primary_record, record_graph) end # Remove duplicate records from all associations if this graph could possibly be a cartesian product eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) if eager_graph[:cartesian_product_number] > 1 # Replace the array of object graphs with an array of model objects record_graphs.replace(records) end # Creates a unique table alias that hasn't already been used in the query. # Will either be the table_alias itself or table_alias_N for some integer # N (starting at 0 and increasing until an unused one is found). def eager_unique_table_alias(ds, table_alias) used_aliases = ds.opts[:from] used_aliases += ds.opts[:join].map{|j| j.table_alias || j.table} if ds.opts[:join] graph = ds.opts[:graph] used_aliases += graph[:table_aliases].keys if graph if used_aliases.include?(table_alias) i = 0 loop do ta = :"#{table_alias}_#{i}" return ta unless used_aliases.include?(ta) i += 1 end end table_alias end private # Make sure the association is valid for this model, and return the related AssociationReflection. def check_association(model, association) raise(Sequel::Error, 'Invalid association') 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 # Build associations for the current object. This is called recursively # to build all dependencies. def eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, current, record_graph) return if dependency_map.empty? # Don't clobber the instance variable array for *_to_many associations if it has already been setup dependency_map.keys.each do |ta| assoc_name = alias_map[ta] current.associations[assoc_name] = type_map[ta] ? [] : nil unless current.associations.include?(assoc_name) end dependency_map.each do |ta, deps| next unless rec = record_graph[ta] key = rec.pk_or_nil || rec.values.sort_by{|x| x[0].to_s} if cached_rec = records_map[ta][key] rec = cached_rec else records_map[ta][key] = rec end assoc_name = alias_map[ta] if type_map[ta] current.associations[assoc_name].push(rec) if reciprocal = reciprocal_map[ta] rec.associations[reciprocal] = current end else current.associations[assoc_name] = rec end # Recurse into dependencies of the current object eager_graph_build_associations_graph(deps, alias_map, type_map, reciprocal_map, records_map, rec, record_graph) 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 eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) records.each do |record| dependency_map.each do |ta, deps| list = record.send(alias_map[ta]) list = if type_map[ta] list.uniq! else [list] if list end # Recurse into dependencies eager_graph_make_associations_unique(list, deps, alias_map, type_map) if list end end end # Qualify the given expression if necessary. The only expressions which are qualified are # unqualified symbols and identifiers, either of which may by sorted. def eager_graph_qualify_order(table_alias, expression) case expression when Symbol table, column, aliaz = split_symbol(expression) raise(Sequel::Error, "Can't use an aliased expression in the :order option") if aliaz table ? expression : Sequel::SQL::QualifiedIdentifier.new(table_alias, expression) when Sequel::SQL::Identifier Sequel::SQL::QualifiedIdentifier.new(table_alias, expression) when Sequel::SQL::OrderedExpression Sequel::SQL::OrderedExpression.new(eager_graph_qualify_order(table_alias, expression.expression), expression.descending) else expression 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)} # Populate keys to monitor reflections.each{|reflection| key_hash[reflection.eager_loader_key] ||= Hash.new{|h,k| h[k] = []}} # Associate each object with every key being monitored a.each do |rec| key_hash.each do |key, id_map| id_map[rec[key]] << rec if rec[key] end end reflections.each do |r| r[:eager_loader].call(key_hash, a, eager_assoc[r[:name]]) a.each{|object| object.send(:run_association_callbacks, r, :after_load, object.associations[r[:name]])} unless r[:after_load].empty? end end # 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 end end end