Какие-либо плагины / драгоценные камни для обнаружения осиротевших записей? - PullRequest
3 голосов
/ 02 января 2011

Ищет что-то, что может пройти через отношения, определенные в моделях, и может проверить БД на наличие потерянных записей / неработающих ссылок между таблицами.

Ответы [ 8 ]

3 голосов
/ 08 октября 2012

(для последней версии скрипта ниже см. https://gist.github.com/KieranP/3849777)

Проблема со скриптом Мартина заключается в том, что он использует ActiveRecord, чтобы сначала извлекать записи, затем находить ассоциации, а затем извлекать ассоциации. Он генерируеттонна SQL-запросов для каждой из ассоциаций. Это не плохо для небольшого приложения, но если у вас есть несколько таблиц с записями по 100 тыс. и каждая с 5+ принадлежащим_, для завершения может потребоваться более 10 минут.

В следующем сценарии вместо этого используется SQL, который ищет потерянные ассоциации принадлежащих принадлежностей для всех моделей в приложении / моделях в приложении Rails. Он обрабатывает простые вызовы принадлежащих принадлежностей принадлежащих и принадлежащих с использованием: имя_класса и полиморфных принадлежащих.используя его, он сократил время выполнения слегка модифицированной версии скрипта Мартина с 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
2 голосов
/ 02 января 2011

Это может зависеть от того, какие действия вы хотите предпринять с сиротами. Возможно, вы просто хотите удалить их? Это можно легко решить с помощью пары SQL-запросов.

1 голос
/ 12 ноября 2014

Была та же самая задача и с текущими искателями, закончившимися следующим образом:

Product.where.not(category_id: Category.pluck("id")).delete_all

, чтобы избавиться от всех Продуктов, которые тем временем потеряли свою категорию.

1 голос
/ 20 августа 2013

Ответ KieranP мне очень помог, но его скрипт не обрабатывает классы с пространством имен.Я добавил несколько строк, игнорируя каталог проблем.Я также добавил необязательный аргумент командной строки DELETE = true, если вы хотите уничтожить все потерянные записи.

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
1 голос
/ 18 июня 2013

Допустим, у вас есть приложение, в котором пользователь может подписаться на журнал.С ассоциациями 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

К сожалению, кто-то забыл добавить зависимые:: destroy в has_many: подписки.Когда пользователь или журнал был удален, оставленная бесхозной подписка осталась позади.

Эта проблема была исправлена ​​зависимыми:: destroy, но все еще оставалось большое количество потерянных записей.Существует два способа удаления потерянных записей.

Подход 1 - Неприятный запах

Subscription.find_each do |subscription|
  if subscription.magazine.nil? || subscription.user.nil?
    subscription.destroy
  end
end

При этом выполняется отдельный SQL-запрос для каждой записи, проверяется, является ли она потерянной, иуничтожает его, если он есть.

Подход 2 - Хороший запах

Subscription.where([
  "user_id NOT IN (?) OR magazine_id NOT IN (?)",
  User.pluck("id"),
  Magazine.pluck("id")
]).destroy_all

Этот подход сначала получает идентификаторы всех пользователей и журналов, а затем выполняет один запрос, чтобы найти все подписки, которые непринадлежат либо пользователю, либо запросу.

1 голос
/ 21 мая 2012

Вы можете создать задачу 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
0 голосов
/ 09 мая 2015

Я написал способ сделать это в моем геме PolyBelongsTo

Вы можете найти все потерянные записи, вызвав метод pbt_orphans в любой модели ActiveRecord.

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

Возвращает массив записей недопустимых имен моделей ActiveRecord, используемых в ваших записях Story. Записи с типами, такими как ["Object", "Class", "Storyable"].

0 голосов
/ 08 апреля 2013

Я создал камень под названием OrphanRecords . Предоставляет грабли для показа / удаления потерянных записей. В настоящее время он не поддерживает ассоциацию HABTM, если вы заинтересованы, пожалуйста, не стесняйтесь внести :)

...