寻找能够通过模型中定义的关系的东西,并检查数据库中的孤立记录/表间断开的链接。
答案 0 :(得分:3)
(有关下面脚本的最新版本,请参阅https://gist.github.com/KieranP/3849777)
Martin脚本的问题在于它使用ActiveRecord首先拉取记录,然后找到关联,然后获取关联。它为每个关联生成大量SQL调用。这对于一个小应用程序来说并不坏,但是当你有一个包含10万条记录的多个表并且每个表都有5+ belongs_to时,它可以在10分钟以上的时间内完成。
以下脚本使用SQL代替,为Rails应用程序中的app / models中的所有模型查找孤立的belongs_to关联。它使用:class_name和多态的belongs_to调用处理简单的belongs_to,belongs_to。在我使用的生产数据上,它将Martin脚本的略微修改版本的运行时间从9分钟缩短到仅仅8秒,它发现了与之前相同的所有问题。
享受: - )
task :orphaned_check => :environment do
Dir[Rails.root.join('app/models/*.rb').to_s].each do |filename|
klass = File.basename(filename, '.rb').camelize.constantize
next unless klass.ancestors.include?(ActiveRecord::Base)
orphanes = Hash.new
klass.reflect_on_all_associations(:belongs_to).each do |belongs_to|
assoc_name, field_name = belongs_to.name.to_s, belongs_to.foreign_key.to_s
if belongs_to.options[:polymorphic]
foreign_type_field = field_name.gsub('_id', '_type')
foreign_types = klass.unscoped.select("DISTINCT(#{foreign_type_field})")
foreign_types = foreign_types.collect { |r| r.send(foreign_type_field) }
foreign_types.sort.each do |foreign_type|
related_sql = foreign_type.constantize.unscoped.select(:id).to_sql
finder = klass.unscoped.select(:id).where("#{foreign_type_field} = '#{foreign_type}'")
finder.where("#{field_name} NOT IN (#{related_sql})").each do |orphane|
orphanes[orphane] ||= Array.new
orphanes[orphane] << [assoc_name, field_name]
end
end
else
class_name = (belongs_to.options[:class_name] || assoc_name).classify
related_sql = class_name.constantize.unscoped.select(:id).to_sql
finder = klass.unscoped.select(:id)
finder.where("#{field_name} NOT IN (#{related_sql})").each do |orphane|
orphanes[orphane] ||= Array.new
orphanes[orphane] << [assoc_name, field_name]
end
end
end
orphanes.sort_by { |record, data| record.id }.each do |record, data|
data.sort_by(&:first).each do |assoc_name, field_name|
puts "#{record.class.name}##{record.id} #{field_name} is present, but #{assoc_name} doesn't exist"
end
end
end
end
答案 1 :(得分:2)
这可能取决于你想对孤儿采取什么行动。也许你只是想删除它们?这可以通过几个SQL查询轻松解决。
答案 2 :(得分:1)
您可以创建Rake任务来搜索和处理孤立记录,例如:
namespace :db do
desc "Handle orphans"
task :handle_orphans => :environment do
Dir[Rails.root + "app/models/**/*.rb"].each do |path|
require path
end
ActiveRecord::Base.send(:descendants).each do |model|
model.reflections.each do |association_name, reflection|
if reflection.macro == :belongs_to
model.all.each do |model_instance|
unless model_instance.send(reflection.primary_key_name).blank?
if model_instance.send(association_name).nil?
print "#{model.name} with id #{model_instance.id} has an invalid reference, would you like to handle it? [y/n]: "
case STDIN.gets.strip
when "y", "Y"
# handle it
end
end
end
end
end
end
end
end
end
答案 3 :(得分:1)
假设您有一个用户可以订阅杂志的应用程序。使用ActiveRecord关联,它看起来像这样:
# app/models/subscription.rb
class Subscription < ActiveRecord::Base
belongs_to :magazine
belongs_to :user
end
# app/models/user.rb
class User < ActiveRecord::Base
has_many :subscriptions
has_many :users, through: :subscriptions
end
# app/models/magazine.rb
class Magazine < ActiveRecord::Base
has_many :subscriptions
has_many :users, through: :subscriptions
end
不幸的是,有人忘了将depend :: destroy添加到has_many:subscriptions。删除用户或杂志后,会遗留一个孤立的订阅。
此问题已由依赖:: destroy解决,但仍有大量孤立的记录挥之不去。 您可以使用两种方法删除孤立记录。
Subscription.find_each do |subscription|
if subscription.magazine.nil? || subscription.user.nil?
subscription.destroy
end
end
这会为每条记录执行单独的SQL查询,检查它是否是孤立的,如果是,则将其销毁。
Subscription.where([
"user_id NOT IN (?) OR magazine_id NOT IN (?)",
User.pluck("id"),
Magazine.pluck("id")
]).destroy_all
此方法首先获取所有用户和杂志的ID,然后执行一个查询以查找不属于用户或查询的所有订阅。
答案 4 :(得分:1)
KieranP的答案对我来说是一个很大的帮助,但他的脚本不处理命名空间类。我添加了几行代码,同时忽略了关注目录。如果你想要破坏所有孤立的记录,我还添加了一个可选的DELETE = true命令行arg。
namespace :db do
desc "Find orphaned records. Set DELETE=true to delete any discovered orphans."
task :find_orphans => :environment do
found = false
model_base = Rails.root.join('app/models')
Dir[model_base.join('**/*.rb').to_s].each do |filename|
# get namespaces based on dir name
namespaces = (File.dirname(filename)[model_base.to_s.size+1..-1] || '').split('/').map{|d| d.camelize}.join('::')
# skip concerns folder
next if namespaces == "Concerns"
# get class name based on filename and namespaces
class_name = File.basename(filename, '.rb').camelize
klass = "#{namespaces}::#{class_name}".constantize
next unless klass.ancestors.include?(ActiveRecord::Base)
orphans = Hash.new
klass.reflect_on_all_associations(:belongs_to).each do |belongs_to|
assoc_name, field_name = belongs_to.name.to_s, belongs_to.foreign_key.to_s
if belongs_to.options[:polymorphic]
foreign_type_field = field_name.gsub('_id', '_type')
foreign_types = klass.unscoped.select("DISTINCT(#{foreign_type_field})")
foreign_types = foreign_types.collect { |r| r.send(foreign_type_field) }
foreign_types.sort.each do |foreign_type|
related_sql = foreign_type.constantize.unscoped.select(:id).to_sql
finder = klass.unscoped.where("#{foreign_type_field} = '#{foreign_type}'")
finder.where("#{field_name} NOT IN (#{related_sql})").each do |orphan|
orphans[orphan] ||= Array.new
orphans[orphan] << [assoc_name, field_name]
end
end
else
class_name = (belongs_to.options[:class_name] || assoc_name).classify
related_sql = class_name.constantize.unscoped.select(:id).to_sql
finder = klass.unscoped
finder.where("#{field_name} NOT IN (#{related_sql})").each do |orphan|
orphans[orphan] ||= Array.new
orphans[orphan] << [assoc_name, field_name]
end
end
end
orphans.sort_by { |record, data| record.id }.each do |record, data|
found = true
data.sort_by(&:first).each do |assoc_name, field_name|
puts "#{record.class.name}##{record.id} #{field_name} is present, but #{assoc_name} doesn't exist" + (ENV['DELETE'] ? ' -- deleting' : '')
record.delete if ENV['DELETE']
end
end
end
puts "No orphans found" unless found
end
end
答案 5 :(得分:1)
相同的任务和当前的发现者结束了:
Product.where.not(category_id: Category.pluck("id")).delete_all
摆脱所有同时失去类别的产品。
答案 6 :(得分:0)
我创建了一个名为OrphanRecords的宝石。它提供了用于显示/删除孤立记录的rake任务。目前它不支持HABTM协会,如果您有兴趣,请随时contribute:)
答案 7 :(得分:0)
我已经在我的宝石PolyBelongsTo
中写了一个方法来做这件事您可以通过在任何ActiveRecord模型上调用 pbt_orphans 方法来查找所有孤立记录。
的Gemfile
gem 'poly_belongs_to'
代码示例
User.pbt_orphans
# => #<ActiveRecord::Relation []> # nil for objects without belongs_to
Story.pbt_orphans
# => #<ActiveRecord::Relation []> # nil for objects without belongs_to
返回所有孤立的记录。
如果您只是想检查单个记录是否是孤立的,您可以使用:orphan?方法执行此操作。
User.first.orphan?
Story.find(5).orphan?
适用于多态关系和非多态关系。
如果您想查找包含无效类型的多态记录,可以执行以下操作:
Story.pbt_mistyped
返回Story记录中使用的无效ActiveRecord模型名称的记录数组。记录类型如[&#34;对象&#34;,&#34;类&#34;,&#34;故事&#34;]。