module ReeDao class Association include Ree::LinkDSL include ReeDao::AssociationMethods link :group_by, from: :ree_array link :index_by, from: :ree_array attr_reader :parent, :parent_dao, :list, :global_opts contract(ReeDao::Associations, Nilor[Sequel::Dataset], Array, Ksplat[RestKeys => Any] => Any) def initialize(parent, parent_dao, list, **global_opts) @parent = parent @parent_dao = parent_dao @list = list @global_opts = global_opts end contract( Or[:belongs_to, :has_one, :has_many, :field], Symbol, Ksplat[RestKeys => Any], Optblock => Array ) def load(assoc_type, assoc_name, **__opts, &block) load_association(assoc_type, assoc_name, **__opts, &block) end def handle_field(field_proc) field_proc.call end contract( Or[:belongs_to, :has_one, :has_many], Symbol, Ksplat[RestKeys => Any], Optblock => Array ) def load_association(assoc_type, assoc_name, **__opts, &block) __opts[:autoload_children] ||= false assoc_index = load_association_by_type( assoc_type, assoc_name, **__opts ) scope = __opts[:scope] dao = if scope.is_a?(Array) return [] if scope.empty? nil else find_dao(assoc_name, parent, scope) end process_block(assoc_index, __opts[:autoload_children], __opts[:to_dto], dao, &block) if block_given? list end contract( Or[:belongs_to, :has_one, :has_many, :field], Symbol, Ksplat[RestKeys => Any] => Any ) def load_association_by_type(type, assoc_name, **__opts) case type when :belongs_to one_to_one( parent_dao, assoc_name, list, scope: __opts[:scope], primary_key: __opts[:primary_key], foreign_key: __opts[:foreign_key], setter: __opts[:setter], reverse: false ) when :has_one one_to_one( parent_dao, assoc_name, list, scope: __opts[:scope], primary_key: __opts[:primary_key], foreign_key: __opts[:foreign_key], to_dto: __opts[:to_dto], setter: __opts[:setter], reverse: true ) when :has_many one_to_many( parent_dao, assoc_name, list, scope: __opts[:scope], primary_key: __opts[:primary_key], foreign_key: __opts[:foreign_key], to_dto: __opts[:to_dto], setter: __opts[:setter] ) end end contract(Or[Hash, Array], Bool, Nilor[Proc], Sequel::Dataset, Block => Any) def process_block(assoc, autoload_children, to_dto, parent_dao, &block) assoc_list = assoc.is_a?(Array) ? assoc : assoc.values.flatten if to_dto assoc_list = assoc_list.map do |item| to_dto.call(item) end end associations = ReeDao::Associations.new( parent.agg_caller, assoc_list, parent.local_vars, parent_dao, autoload_children, **global_opts ) if parent_dao.nil? || parent_dao.db.in_transaction? || ReeDao::Associations.sync_mode? associations.instance_exec(assoc_list, &block) else threads = associations.instance_exec(assoc_list, &block) threads[:association_threads].map do |association, assoc_type, assoc_name, __opts, block| Thread.new do association.load(assoc_type, assoc_name, **__opts, &block) end end.map(&:join) threads[:field_threads].map do |association, field_proc| Thread.new do association.handle_field(field_proc) end end.map(&:join) end end contract( Nilor[Sequel::Dataset], Symbol, Array, Kwargs[ primary_key: Nilor[Symbol], foreign_key: Nilor[Symbol], scope: Nilor[Sequel::Dataset, Array], setter: Nilor[Or[Symbol, Proc]], to_dto: Nilor[Proc], reverse: Bool ] => Or[Hash, Array] ) def one_to_one(parent_dao, assoc_name, list, scope: nil, primary_key: :id, foreign_key: nil, setter: nil, to_dto: nil, reverse: true) return {} if list.empty? primary_key ||= :id if scope.is_a?(Array) items = scope else assoc_dao = find_dao(assoc_name, parent, scope) if reverse # has_one if !foreign_key if parent_dao.nil? raise ArgumentError.new("foreign_key should be provided for :#{assoc_name} association") end foreign_key = foreign_key_from_dao(parent_dao) end root_ids = list.map(&:id).uniq else # belongs_to if !foreign_key foreign_key = :"#{assoc_name}_id" end root_ids = list.map(&:"#{foreign_key}").compact end scope ||= assoc_dao scope = scope.where((reverse ? foreign_key : :id) => root_ids) items = add_scopes(scope, global_opts[assoc_name]) end assoc = index_by(items) { _1.send(reverse ? foreign_key : :id) } populate_association( list, assoc, assoc_name, setter: setter, reverse: reverse, primary_key: primary_key, foreign_key: foreign_key, to_dto: to_dto ) assoc end contract( Nilor[Sequel::Dataset], Symbol, Array, Kwargs[ foreign_key: Nilor[Symbol], primary_key: Nilor[Symbol], scope: Nilor[Sequel::Dataset, Array], setter: Nilor[Or[Symbol, Proc]], to_dto: Nilor[Proc] ] => Or[Hash, Array] ) def one_to_many(parent_dao, assoc_name, list, primary_key: nil, foreign_key: nil, scope: nil, setter: nil, to_dto: nil) return {} if list.empty? primary_key ||= :id if scope.is_a?(Array) items = scope else assoc_dao = nil assoc_dao = find_dao(assoc_name, parent, scope) if !foreign_key if parent_dao.nil? raise ArgumentError.new("foreign_key should be provided for :#{assoc_name} association") end foreign_key = foreign_key_from_dao(parent_dao) end root_ids = list.map(&:"#{primary_key}") scope ||= assoc_dao scope = scope.where(foreign_key => root_ids) items = add_scopes(scope, global_opts[assoc_name]) end assoc = if foreign_key group_by(items) { _1.send(foreign_key) } else items end populate_association( list, assoc, assoc_name, setter: setter, primary_key: primary_key, foreign_key: foreign_key, to_dto: to_dto, multiple: true ) assoc end contract( Array, Or[Hash, Array], Symbol, Kwargs[ primary_key: Nilor[Symbol], foreign_key: Nilor[Symbol], reverse: Nilor[Bool], setter: Nilor[Or[Symbol, Proc]], to_dto: Nilor[Proc], multiple: Bool ] => Any ) def populate_association(list, association_index, assoc_name, primary_key: nil, foreign_key: nil, reverse: nil, setter: nil, to_dto: nil, multiple: false) assoc_setter = if setter setter else "set_#{assoc_name}" end list.each do |item| if setter && setter.is_a?(Proc) if to_dto assoc_index = {} association_index.each do |key, value| if value.is_a?(Array) assoc_index[key] = value.map { to_dto.call(_1) } else assoc_index[key] = to_dto.call(value) end end self.instance_exec(item, assoc_index, &assoc_setter) else self.instance_exec(item, association_index, &assoc_setter) end else key = if reverse.nil? primary_key else if reverse primary_key else foreign_key ? foreign_key : "#{assoc_name}_id" end end value = association_index[item.send(key)] if to_dto && !value.nil? value = if value.is_a?(Array) value.map { to_dto.call(_1) } else to_dto.call(value) end end value = [] if value.nil? && multiple begin item.send(assoc_setter, value) rescue NoMethodError item.send("#{assoc_name}=", value) end end end end contract(Nilor[Sequel::Dataset], Nilor[Proc] => Array) def add_scopes(scope, named_scope) res = if named_scope named_scope.call(scope) else scope end res.all end def method_missing(method, *args, &block) return super if !parent.agg_caller.private_methods(false).include?(method) parent.agg_caller.send(method, *args, &block) end private def foreign_key_from_dao(dao) "#{dao.first_source_table.to_s.gsub(/s$/, '')}_id".to_sym end end end