`
hideto
  • 浏览: 2678366 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Rails源码研究之ActiveRecord:二,Associations

    博客分类:
  • Ruby
阅读更多
今天学习一下ActiveRecord的Associations相关的源码,即了解一下我们常用的has_many、has_one、belongs_to、has_and_belongs_to_many的原理

1,activerecord-1.15.3\lib\active_record\associations.rb:
require 'active_record/associations/association_proxy'
require 'active_record/associations/association_collection'
require 'active_record/associations/belongs_to_association'
require 'active_record/associations/belongs_to_polymorphic_association'
require 'active_record/associations/has_one_association'
require 'active_record/associations/has_many_association'
require 'active_record/associations/has_many_through_association'
require 'active_record/associations/has_and_belongs_to_many_association'
require 'active_record/deprecated_associations'

module ActiveRecord
  module Associations
    module ClassMethods
      def has_many(association_id, options = {}, &extension)
        reflection = create_has_many_reflection(association_id, options, &extension)

        configure_dependency_for_has_many(reflection)

        if options[:through]
          collection_reader_method(reflection, HasManyThroughAssociation)
        else
          add_multiple_associated_save_callbacks(reflection.name)
          add_association_callbacks(reflection.name, reflection.options)
          collection_accessor_methods(reflection, HasManyAssociation)
        end

        add_deprecated_api_for_has_many(reflection.name)
      end

      def has_one(association_id, options = {})
        reflection = create_has_one_reflection(association_id, options)

        module_eval do
          after_save <<-EOF
            association = instance_variable_get("@#{reflection.name}")
            if !association.nil? && (new_record? || association.new_record? || association["#{reflection.primary_key_name}"] != id)
              association["#{reflection.primary_key_name}"] = id
              association.save(true)
            end
          EOF
        end
      
        association_accessor_methods(reflection, HasOneAssociation)
        association_constructor_method(:build,  reflection, HasOneAssociation)
        association_constructor_method(:create, reflection, HasOneAssociation)
        
        configure_dependency_for_has_one(reflection)

        # deprecated api
        deprecated_has_association_method(reflection.name)
        deprecated_association_comparison_method(reflection.name, reflection.class_name)
      end

      def belongs_to(association_id, options = {})
        if options.include?(:class_name) && !options.include?(:foreign_key)
          ::ActiveSupport::Deprecation.warn(
          "The inferred foreign_key name will change in Rails 2.0 to use the association name instead of its class name when they differ.  When using :class_name in belongs_to, use the :foreign_key option to explicitly set the key name to avoid problems in the transition.",
          caller)
        end
        
        reflection = create_belongs_to_reflection(association_id, options)
        
        if reflection.options[:polymorphic]
          association_accessor_methods(reflection, BelongsToPolymorphicAssociation)

          module_eval do
            before_save <<-EOF
              association = instance_variable_get("@#{reflection.name}")
              if association && association.target
                if association.new_record?
                  association.save(true)
                end
                
                if association.updated?
                  self["#{reflection.primary_key_name}"] = association.id
                  self["#{reflection.options[:foreign_type]}"] = association.class.base_class.name.to_s
                end
              end
            EOF
          end
        else
          association_accessor_methods(reflection, BelongsToAssociation)
          association_constructor_method(:build,  reflection, BelongsToAssociation)
          association_constructor_method(:create, reflection, BelongsToAssociation)

          module_eval do
            before_save <<-EOF
              association = instance_variable_get("@#{reflection.name}")
              if !association.nil? 
                if association.new_record?
                  association.save(true)
                end
                
                if association.updated?
                  self["#{reflection.primary_key_name}"] = association.id
                end
              end            
            EOF
          end
      
          # deprecated api
          deprecated_has_association_method(reflection.name)
          deprecated_association_comparison_method(reflection.name, reflection.class_name)
        end

        if options[:counter_cache]
          cache_column = options[:counter_cache] == true ?
            "#{self.to_s.underscore.pluralize}_count" :
            options[:counter_cache]

          module_eval(
            "after_create '#{reflection.name}.class.increment_counter(\"#{cache_column}\", #{reflection.primary_key_name})" +
            " unless #{reflection.name}.nil?'"
          )

          module_eval(
            "before_destroy '#{reflection.name}.class.decrement_counter(\"#{cache_column}\", #{reflection.primary_key_name})" +
            " unless #{reflection.name}.nil?'"
          )          
        end
      end

      def has_and_belongs_to_many(association_id, options = {}, &extension)
        reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
        
        add_multiple_associated_save_callbacks(reflection.name)
        collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)

        old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
        class_eval <<-end_eval
          alias_method :#{old_method}, :destroy_without_callbacks
          def destroy_without_callbacks
            #{reflection.name}.clear
            #{old_method}
          end
        end_eval

        add_association_callbacks(reflection.name, options)
        
        # deprecated api
        deprecated_collection_count_method(reflection.name)
        deprecated_add_association_relation(reflection.name)
        deprecated_remove_association_relation(reflection.name)
        deprecated_has_collection_method(reflection.name)
      end

      private
        def association_accessor_methods(reflection, association_proxy_class)
          define_method(reflection.name) do |*params|
            force_reload = params.first unless params.empty?
            association = instance_variable_get("@#{reflection.name}")

            if association.nil? || force_reload
              association = association_proxy_class.new(self, reflection)
              retval = association.reload
              if retval.nil? and association_proxy_class == BelongsToAssociation
                instance_variable_set("@#{reflection.name}", nil)
                return nil
              end
              instance_variable_set("@#{reflection.name}", association)
            end

            association.target.nil? ? nil : association
          end

          define_method("#{reflection.name}=") do |new_value|
            association = instance_variable_get("@#{reflection.name}")
            if association.nil?
              association = association_proxy_class.new(self, reflection)
            end

            association.replace(new_value)

            unless new_value.nil?
              instance_variable_set("@#{reflection.name}", association)
            else
              instance_variable_set("@#{reflection.name}", nil)
              return nil
            end

            association
          end

          define_method("set_#{reflection.name}_target") do |target|
            return if target.nil? and association_proxy_class == BelongsToAssociation
            association = association_proxy_class.new(self, reflection)
            association.target = target
            instance_variable_set("@#{reflection.name}", association)
          end
        end

        def collection_reader_method(reflection, association_proxy_class)
          define_method(reflection.name) do |*params|
            force_reload = params.first unless params.empty?
            association = instance_variable_get("@#{reflection.name}")

            unless association.respond_to?(:loaded?)
              association = association_proxy_class.new(self, reflection)
              instance_variable_set("@#{reflection.name}", association)
            end

            association.reload if force_reload

            association
          end
        end

        def collection_accessor_methods(reflection, association_proxy_class)
          collection_reader_method(reflection, association_proxy_class)

          define_method("#{reflection.name}=") do |new_value|
            # Loads proxy class instance (defined in collection_reader_method) if not already loaded
            association = send(reflection.name) 
            association.replace(new_value)
            association
          end

          define_method("#{reflection.name.to_s.singularize}_ids") do
            send(reflection.name).map(&:id)
          end

          define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
            ids = (new_value || []).reject { |nid| nid.blank? }
            send("#{reflection.name}=", reflection.class_name.constantize.find(ids))
          end
        end

        def association_constructor_method(constructor, reflection, association_proxy_class)
          define_method("#{constructor}_#{reflection.name}") do |*params|
            attributees      = params.first unless params.empty?
            replace_existing = params[1].nil? ? true : params[1]
            association      = instance_variable_get("@#{reflection.name}")

            if association.nil?
              association = association_proxy_class.new(self, reflection)
              instance_variable_set("@#{reflection.name}", association)
            end

            if association_proxy_class == HasOneAssociation
              association.send(constructor, attributees, replace_existing)
            else
              association.send(constructor, attributees)
            end
          end
        end

        def create_has_many_reflection(association_id, options, &extension)
          options.assert_valid_keys(
            :class_name, :table_name, :foreign_key,
            :exclusively_dependent, :dependent,
            :select, :conditions, :include, :order, :group, :limit, :offset,
            :as, :through, :source, :source_type,
            :uniq,
            :finder_sql, :counter_sql, 
            :before_add, :after_add, :before_remove, :after_remove, 
            :extend
          )

          options[:extend] = create_extension_module(association_id, extension) if block_given?

          create_reflection(:has_many, association_id, options, self)
        end

        def create_has_one_reflection(association_id, options)
          options.assert_valid_keys(
            :class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as
          )

          create_reflection(:has_one, association_id, options, self)
        end

        def create_belongs_to_reflection(association_id, options)
          options.assert_valid_keys(
            :class_name, :foreign_key, :foreign_type, :remote, :conditions, :order, :include, :dependent, 
            :counter_cache, :extend, :polymorphic
          )
          
          reflection = create_reflection(:belongs_to, association_id, options, self)

          if options[:polymorphic]
            reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
          end

          reflection
        end
        
        def create_has_and_belongs_to_many_reflection(association_id, options, &extension)
          options.assert_valid_keys(
            :class_name, :table_name, :join_table, :foreign_key, :association_foreign_key, 
            :select, :conditions, :include, :order, :group, :limit, :offset,
            :uniq, 
            :finder_sql, :delete_sql, :insert_sql,
            :before_add, :after_add, :before_remove, :after_remove, 
            :extend
          )

          options[:extend] = create_extension_module(association_id, extension) if block_given?

          reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)

          reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
          
          reflection
        end
    end
  end
end

该文件主要定义了has_many、has_one、belongs_to、has_and_belongs_to_many这四种方法并将它们分别代理到各个具体的关联类

2,activerecord-1.15.3\lib\active_record\associations\association_collection.rb:
module ActiveRecord
  module Associations
    class AssociationCollection < AssociationProxy

      def <<(*records)
        result = true
        load_target

        @owner.transaction do
          flatten_deeper(records).each do |record|
            raise_on_type_mismatch(record)
            callback(:before_add, record)
            result &&= insert_record(record) unless @owner.new_record?
            @target << record
            callback(:after_add, record)
          end
        end

        result && self
      end

      alias_method :push, :<<
      alias_method :concat, :<<

      def delete_all
        load_target
        delete(@target)
        reset_target!
      end

      def delete(*records)
        records = flatten_deeper(records)
        records.each { |record| raise_on_type_mismatch(record) }
        records.reject! { |record| @target.delete(record) if record.new_record? }
        return if records.empty?
        
        @owner.transaction do
          records.each { |record| callback(:before_remove, record) }
          delete_records(records)
          records.each do |record|
            @target.delete(record)
            callback(:after_remove, record)
          end
        end
      end

      def clear
        return self if length.zero? # forces load_target if hasn't happened already

        if @reflection.options[:dependent] && @reflection.options[:dependent] == :delete_all
          destroy_all
        else          
          delete_all
        end

        self
      end
      
      def destroy_all
        @owner.transaction do
          each { |record| record.destroy }
        end

        reset_target!
      end

      def create(attributes = {})
        if attributes.is_a?(Array)
          attributes.collect { |attr| create(attr) }
        else
          record = build(attributes)
          record.save unless @owner.new_record?
          record
        end
      end

      protected
        def find_target
          records =
            if @reflection.options[:finder_sql]
              @reflection.klass.find_by_sql(@finder_sql)
            else
              find(:all)
            end

          @reflection.options[:uniq] ? uniq(records) : records
        end
    end
  end
end

AssociationCollection是HasOneAssociation、HasManyAssociation、HasManyThroughAssociation、BelongsToAssociation、HasAndBelongsToManyAssociation、
BelongsToPolymorphicAssociation这六种关联类的父类,其中定义了关联的<<、create、delete、clear等方法

3,activerecord-1.15.3\lib\active_record\reflection.rb:
module ActiveRecord
  module Reflection

    module ClassMethods
      def create_reflection(macro, name, options, active_record)
        case macro
          when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
            reflection = AssociationReflection.new(macro, name, options, active_record)
          when :composed_of
            reflection = AggregateReflection.new(macro, name, options, active_record)
        end
        write_inheritable_hash :reflections, name => reflection
        reflection
      end
    end

    class MacroReflection
      attr_reader :active_record
      def initialize(macro, name, options, active_record)
        @macro, @name, @options, @active_record = macro, name, options, active_record
      end
    end

    class AggregateReflection < MacroReflection #:nodoc:
      def klass
        @klass ||= Object.const_get(options[:class_name] || class_name)
      end
    end

    class AssociationReflection < MacroReflection #:nodoc:
      def klass
        @klass ||= active_record.send(:compute_type, class_name)
      end
    end

  end
end

该文件定义了reflection变量以及AggregateReflection、AssociationReflection类,我们在关联类的build方法里可以看到record = @reflection.klass.new(attributes)的调用

4,activerecord-1.15.3\lib\active_record\associations\has_many_association.rb:
module ActiveRecord
  module Associations
    class HasManyAssociation < AssociationCollection
      def build(attributes = {})
        if attributes.is_a?(Array)
          attributes.collect { |attr| build(attr) }
        else
          record = @reflection.klass.new(attributes)
          set_belongs_to_association_for(record)
          
          @target ||= [] unless loaded?
          @target << record
          
          record
        end
      end

      def count(*args)
        if @reflection.options[:counter_sql]
          @reflection.klass.count_by_sql(@counter_sql)
        elsif @reflection.options[:finder_sql]
          @reflection.klass.count_by_sql(@finder_sql)
        else
          column_name, options = @reflection.klass.send(:construct_count_options_from_legacy_args, *args)          
          options[:conditions] = options[:conditions].nil? ?
            @finder_sql :
            @finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
          options[:include] = @reflection.options[:include]

          @reflection.klass.count(column_name, options)
        end
      end

      def find(*args)
        options = Base.send(:extract_options_from_args!, args)

        if @reflection.options[:finder_sql]
          expects_array = args.first.kind_of?(Array)
          ids = args.flatten.compact.uniq

          if ids.size == 1
            id = ids.first
            record = load_target.detect { |record| id == record.id }
            expects_array ? [ record ] : record
          else
            load_target.select { |record| ids.include?(record.id) }
          end
        else
          conditions = "#{@finder_sql}"
          if sanitized_conditions = sanitize_sql(options[:conditions])
            conditions << " AND (#{sanitized_conditions})"
          end
          options[:conditions] = conditions

          if options[:order] && @reflection.options[:order]
            options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
          elsif @reflection.options[:order]
            options[:order] = @reflection.options[:order]
          end

          merge_options_from_reflection!(options)

          # Pass through args exactly as we received them.
          args << options
          @reflection.klass.find(*args)
        end
      end

      protected
        def method_missing(method, *args, &block)
          if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
            super
          else
            create_scoping = {}
            set_belongs_to_association_for(create_scoping)

            @reflection.klass.with_scope(
              :create => create_scoping,
              :find => {
                :conditions => @finder_sql, 
                :joins      => @join_sql, 
                :readonly   => false
              }
            ) do
              @reflection.klass.send(method, *args, &block)
            end
          end
        end

    end
  end
end

HasManyAssociation等具体的关联类里面就定义了一些自己的find、count、build等方法

总体说来,ActiveRecord首先定义has_many、belongs_to等方法,并利用反射得到这些方法的参数所代表的类,然后把对这些类的操作代理到具体的HasManyAssociation等关联类
了解了ActiveRecord的associations,我们可以轻松定制自己的关联方法和关联类
分享到:
评论
2 楼 hideto 2007-06-21  
就是让你自己看
1 楼 fredzhang 2007-06-21  
光把代码罗列了一遍,什么都没讲,还不如自己看

相关推荐

    rails_async_migrations:对ActiveRecord :: Migration的异步支持

    RailsAsyncMigrations ActiveRecord::Migration扩展程序以一种简单直接的方式使您的迁移异步。动机创建该库的目的是为了帮助在技术水平上难以扩展的小型公司。 小型项目不需要异步迁移队列,大公司在遇到扩展问题时...

    Rails的精简版本Rails::API.zip

    Rails::API 是 Rails 的精简版本,针对不需要使用完整 Rails 功能的开发者。 Rails::API 移除了 ActionView 和其他一些渲染功能,不关心Web前端的开发者可更容易、快速地开发应用程序,因此运行速度比正常的 Rails ...

    userstamp, 这个 Rails 插件扩展ActiveRecord.zip

    userstamp, 这个 Rails 插件扩展ActiveRecord Userstamp插件( v-2.0 )概述Userstamp插件扩展了 ActiveRecord::Base,以添加对'创建者','更新程序'和'deleter'属性的自动更新。 它是基于 ActiveRecord::Timesta

    to_xls-rails:将Rails ActiveRecord或Mongid数据导出到Excel文件

    数组元素支持对象:ActiveRecord,Mongid,哈希。 在您的Gemfile中: gem 'to_xls-rails' # Last officially released gem # gem "to_xls-rails", :git =&gt; "git://github....

    ROR绿色最新环境(2013/3/10)

    Rails::Rack::Logger ActionDispatch::ShowExceptions ActionDispatch::DebugExceptions ActionDispatch::RemoteIp ActionDispatch::Reloader ActionDispatch::Callbacks ActiveRecord::ConnectionAdapters::...

    ActiveRecord简单实例_activerecord.zip

    在Ruby on Rails框架中,ActiveRecord是一个至关重要的组件,它负责模型(Model)与数据库之间的交互。本实例将深入探讨ActiveRecord的基本用法,帮助理解如何在实际开发中有效地运用这个强大的工具。 首先,让我们...

    ActiveRecord-Without-Rails:只是在没有Rails的情况下使用ActiveRecord迁移的简单示例

    没有Rails的ActiveRecord 只是在没有Rails的情况下使用ActiveRecord迁移的简单示例您可以执行的任务: rake db:create rake db:migrate rake db:dropRails 5+的注意事项请注意,即使使用Rails 5,您也需要rake db:...

    stateful_enum:基于ActiveRecord :: Enum构建的非常简单的状态机插件

    stateful_enum是建立在ActiveRecord的内置ActiveRecord :: Enum之上的状态机gem。 安装 将此行添加到您的Rails应用程序的Gemfile中: gem 'stateful_enum' 和捆绑。 动机 您不需要抽象 stateful_enum取决于...

    Ruby on Rails入门经典代码

    Ruby on Rails,简称Rails,是基于Ruby语言的一个开源Web应用程序框架,它遵循MVC(Model-View-Controller)架构模式,旨在使Web开发过程更加高效、简洁。本压缩包中的"Ruby on Rails入门经典代码"提供了新手学习...

    graphql-query-resolver:最小化由GraphQL和ActiveRecord生成的N + 1个查询

    GraphQL :: QueryResolver GraphQL :: QueryResolver是一个附加,它可使... 然后,每个匹配的选择都将传递到ActiveRecord::Associations::Preloader.new因此您的查询现在仅对GraphQL AST的每个级别发出一个SELECT语句。

    Rails项目源代码

    这个Rails项目提供了学习和研究Web开发的机会,特别是对于Ruby on Rails新手,可以通过阅读和理解源代码来提升技能,了解实际应用中Rails的用法。同时,对于有经验的开发者,这个项目也可以作为一个起点,进行二次...

    Ruby on Rails中的ActiveRecord编程指南

    ### Ruby on Rails中的ActiveRecord编程指南 #### 一、引言 在Ruby on Rails框架中,ActiveRecord是一种用于实现数据库抽象层的对象关系映射(ORM)工具。它为开发人员提供了一种简单而强大的方式来处理数据库记录...

    paranoid2:Rails 4 的偏执模型

    Rails 4 定义了ActiveRecord::Base#destroy! 所以Paranoid2 gem 使用force: true arg 来强制销毁。 安装 将此行添加到应用程序的 Gemfile 中: gem 'paranoid2' 然后执行: $ bundle 用法 将deleted_at: ...

    rails-exporter-源码.rar

    《Rails Exporter 源码解析》 Rails Exporter 是一个用于 Rails 应用程序的开源工具,主要用于数据导出功能。源码分析将帮助我们深入理解其内部工作原理,以便更好地利用它来优化我们的应用。 一、Rails 框架基础 ...

    hashid-rails:在Rails应用程序ActiveRecord模型中使用Hashids(http:hashids.orgruby)

    在要启用哈希值的ActiveRecord模型中包括Hashid Rails。 class Model &lt; ActiveRecord :: Base include Hashid :: Rails end 继续使用Model#find输入hashid或常规的'ol id。 @person = Person . find ( params...

    Rails3的ActiveRecord 查询API.doc

    Rails 3.1 及以后版本的 ActiveRecord 查询API发生了重大变化,主要目的是为了提供更清晰、更可维护的代码,并且提高性能。在 Rails 2.x 中,许多使用哈希参数的查询方法如 `:conditions`, `:include`, `:joins` 等...

    workflow-activerecord:工作流库的 ActiveRecordRails 集成

    工作流库的 ActiveRecord/Rails 集成 工作流活动记录的主要+次要版本基于最旧的兼容 ActiveRecord API。 要在 Rails/ActiveRecord 4.1、4.2、5.0、5.1、5.2、6.0、6.1 中使用 ,请使用: gem 'workflow-...

    flowdock-rails:用于 ActiveRecord 模型的 Flowdock 通知程序

    Flowdock::Rails 这个 gem 添加了一个类方法来向特定流发送通知,以在启用的资源上创建和更新事件安装将此行添加到应用程序的 Gemfile 中: gem 'flowdock-rails'然后执行: $ bundle或者自己安装: $ gem install ...

    Ruby on Rails入门例子

    - **ActiveRecord查询接口(Query Interface)**:ActiveRecord提供了丰富的查询API,如`User.find(1)`, `Post.where(title: 'Hello')`,用于从数据库检索数据。 - ** erb语法**:在视图文件中,我们可以使用erb...

    Rails上的API:使用Rails构建REST APIAPIs on Rails: Building REST APIs with Rails

    1. **快速开发**:Rails内置了许多实用的功能和库,如ActiveRecord ORM、MVC架构等,这些都能够极大地加快开发进度。 2. **代码简洁**:Rails遵循“约定优于配置”的原则,这意味着开发者无需编写大量重复代码就能...

Global site tag (gtag.js) - Google Analytics