acts-as-taggable-on
此gem最基本的用法就是model中调用acts_as_taggable来使model具备被贴标签的能力。跟踪一下该方法:
[6] pry(main)> binding.trace_tree(htmp: 'acts_as_taggable'){Student.acts_as_taggable}
完整调用栈如下
概况如下

源码如下
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>
完整调用栈如下
概况如下

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