此gem最基本的用法就是model中调用acts_as_taggable来使model具备被贴标签的能力。跟踪一下该方法:

[6] pry(main)> binding.trace_tree(htmp: 'acts_as_taggable'){Student.acts_as_taggable}

完整调用栈如下

20180429_211329_129_acts_as_taggable.html

概况如下


源码如下

def taggable_on(preserve_tag_order, *tag_types)
  tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)

  if taggable?
    self.tag_types = (self.tag_types + tag_types).uniq
    self.preserve_tag_order = preserve_tag_order
  else
    class_attribute :tag_types
    self.tag_types = tag_types
    class_attribute :preserve_tag_order
    self.preserve_tag_order = preserve_tag_order

    class_eval do
      has_many :taggings, as: :taggable, dependent: :destroy, class_name: '::ActsAsTaggableOn::Tagging'
      has_many :base_tags, through: :taggings, source: :tag, class_name: '::ActsAsTaggableOn::Tag'

      def self.taggable?
        true
      end
    end
  end

  # each of these add context-specific methods and must be
  # called on each call of taggable_on
  include Core
  include Collection
  include Cache
  include Ownership
  include Related
  include Dirty
end

首先,此module本身有返回false的taggable?方法,这个方法会在使用has_many与标签表完成关联后,被重定义为true。下次再调用acts_as_taggable时,便可是识别出已关联标签表,只需给类属性tag_types增加标签类型,以及修改preserve_tag_order。

至于tag_types是有什么用的呢?从官方文档可见,此gem还有一种用法形如acts_as_taggable_on :skills, :interests,而acts_as_taggable实际上也是执行acts_as_taggable_on :tags。当使用acts_as_taggable_XXX后,model便获得了方法@model.tag_list、@model.skill_list

于是看看tag_list方法是怎么运行的:

[10] pry(main)> st = Student.first
  Student Load (0.1ms)  SELECT  "students".* FROM "students" ORDER BY "students"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<student:0x007fc03b5a9918 name:="" "carl",="" created_at:="" sun,="" 02="" jul="" 2017="" 04:10:00="" utc="" +00:00,="" updated_at:="" mon,="" 06="" nov="" 2017="" 15:30:57="" utc="" +00:00,="" gender:="" "male",="" grade:="" 1,="" alias_names:="" [],="" id:="" 2="">
[11] pry(main)> binding.trace_tree(htmp: 'tag_lst'){st.tag_list}
  ActsAsTaggableOn::Tagging Load (27.1ms)  SELECT "taggings".* FROM "taggings" WHERE "taggings"."taggable_id" = ? AND "taggings"."taggable_type" = ?  [["taggable_id", 2], ["taggable_type", "Student"]]
  ActsAsTaggableOn::Tag Load (33.4ms)  SELECT "tags".* FROM "tags" INNER JOIN "taggings" ON "tags"."id" = "taggings"."tag_id" WHERE "taggings"."taggable_id" = ? AND "taggings"."taggable_type" = ? AND (taggings.context = 'tags' AND taggings.tagger_id IS NULL)  [["taggable_id", 2], ["taggable_type", "Student"]]
=> []</student:0x007fc03b5a9918>

完整调用栈如下

20180429_221810_873_tag_lst.html

概况如下


tag_list方法的定义来源于以下位置。当model类执行acts_as_taggable后,该类会执行ActsAsTaggableOn::Taggable::Core::ClassMethods的initialize_acts_as_taggable_on_core方法。它首先给model类include一个匿名module,然后迭代每一个tag_types,给该匿名module定义#{tag_type}_list方法。于是,当tag_types有tags、skills时,便会获得tag_list、skill_list方法。

此外,还会针对每个tag_type定义两个relation(has_many)。当tag_types有tags、skills时,便会获得tag_taggings、tags、skill_taggings、skills。

多次执行acts_as_taggable_on是没有问题的,因为只是重定义relation和匿名类上的方法

module ActsAsTaggableOn::Taggable
  module Core
    def self.included(base)
      base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods

      base.class_eval do
        attr_writer :custom_contexts
        after_save :save_tags
      end

      base.initialize_acts_as_taggable_on_core
    end


    module ClassMethods
      def initialize_acts_as_taggable_on_core
        include taggable_mixin
        tag_types.map(&:to_s).each do |tags_type|
          tag_type = tags_type.to_s.singularize
          context_taggings = "#{tag_type}_taggings".to_sym
          context_tags = tags_type.to_sym
          taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : [])

          class_eval do
            # when preserving tag order, include order option so that for a 'tags' context
            # the associations tag_taggings & tags are always returned in created order
            has_many context_taggings, -> { includes(:tag).order(taggings_order).where(context: tags_type) },
                     as: :taggable,
                     class_name: ActsAsTaggableOn::Tagging,
                     dependent: :destroy

            has_many context_tags, -> { order(taggings_order) },
                     class_name: ActsAsTaggableOn::Tag,
                     through: context_taggings,
                     source: :tag
          end

          taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
            def #{tag_type}_list
              tag_list_on('#{tags_type}')
            end

            def #{tag_type}_list=(new_tags)
              set_tag_list_on('#{tags_type}', new_tags)
            end

            def all_#{tags_type}_list
              all_tags_list_on('#{tags_type}')
            end
          RUBY
        end
      end


接着看tag_list的实际运行。就是连接tags和taggings两个表,然后将标签名转化成ActsAsTaggableOn::TagList

def tag_list_cache_on(context)
  variable_name = "@#{context.to_s.singularize}_list"
  if instance_variable_get(variable_name)
    instance_variable_get(variable_name)
  elsif cached_tag_list_on(context) && self.class.caching_tag_list_on?(context)
    instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse)
  else
    instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
  end
end

def tags_on(context)
  scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
  # when preserving tag order, return tags in created order
  # if we added the order to the association this would always apply
  scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
  scope
end