Удалить дубликаты записей / объектов, однозначно идентифицируемых несколькими атрибутами - PullRequest
1 голос
/ 08 мая 2010

У меня есть модель HeroStatus со следующими атрибутами:

  • ID
  • user_id
  • recordable_type
  • hero_type (может быть NULL!)
  • recordable_id
  • created_at

Существует более 100 hero_statuses, и пользователь может иметь много hero_statuses, но не может иметь один и тот же hero_status несколько раз.

Hero_status пользователя уникально идентифицируется комбинацией recordable_type + hero_type + recordable_id. По сути, я пытаюсь сказать, что для конкретного пользователя не может быть дубликата hero_status.

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

user_id = 18
recordable_type = 'Evil'
hero_type = 'Halitosis'
recordable_id = 1
created_at = '2010-05-03 18:30:30'

user_id = 18
recordable_type = 'Evil'
hero_type = 'Halitosis'
recordable_id = 1
created_at = '2009-03-03 15:30:00'

user_id = 18
recordable_type = 'Good'
hero_type = 'Hugs'
recordable_id = 1
created_at = '2009-02-03 12:30:00'

user_id = 18
recordable_type = 'Good'
hero_type = NULL
recordable_id = 2
created_at = '2009-012-03 08:30:00'

(Последние два, очевидно, не дураки. Первые два.) Итак, я хочу избавиться от дубликата hero_status. Который из? Тот, с самой последней датой.

У меня три вопроса:

  1. Как удалить дубликаты, используя только SQL-подход?

  2. Как мне удалить дубликаты, используя чистый раствор Ruby? Нечто похожее на это: Удаление «дубликатов объектов» .

  3. Как установить валидацию для предотвращения дублирования записей в будущем?

Ответы [ 2 ]

1 голос
/ 08 мая 2010

Для подхода только на SQL я бы использовал этот запрос - (я предполагаю, что идентификаторы уникальны.)

DELETE FROM HeroStatus WHERE id IN
(SELECT id FROM 
   (SELECT user_id, recordable_type, hero_type, recordable_id, MAX(created_at)
     GROUP BY del.user_id, recordable_type, hero_type, recordable_id
     HAVING Count(id)>1) AS del 
      INNER JOIN HeroStatus AS hs ON
      hs.user_id=del.user_id AND hs.recordable_type=del.recordable_type 
       AND hs.hero_type=del.hero_type AND hs.recordable_id=del.recordable_id 
       AND hs.created_at = del.created_at)

Немного монстра! Запрос находит все дубликаты, используя естественный ключ (user_id, recordable_type, hero_type), и выбирает тот, который имеет наибольшее значение created_at (последний создан). Затем он находит идентификаторы этих строк (путем присоединения к основной таблице) и удаляет строки с этим идентификатором.

(Пожалуйста, сначала попробуйте это на копии таблицы и убедитесь, что вы получите желаемый результат!: -)

Чтобы предотвратить это в будущем, добавьте уникальный индекс или ограничение по столбцам user_id, recordable_type, hero_type, recordable_id. Э.Г.

ALTER TABLE HeroStatus 
ADD UNIQUE (user_id, recordable_type, hero_type, recordable_id)

EDIT:

Вы добавляете (и удаляете) этот индекс в рамках миграции следующим образом:

add_index(:HeroStatus, [:user_id, :recordable_type, :hero_type, :recordable_id], :unique => true)
remove_index(:HeroStatus, :column => [:user_id, :recordable_type, :hero_type, :recordable_id], :unique => true)

Или, если вы хотите явно назвать его:

add_index(:HeroStatus, [:user_id, :recordable_type, :hero_type, :recordable_id], :unique => true, :name => :my_unique_index)
remove_index(:HeroStatus, :name => :my_unique_index)
0 голосов
/ 08 мая 2010

Иногда вам нужно просто закатать рукава и сделать какой-нибудь серьезный SQL, чтобы уничтожить все, что вам не нужно.Это легко, если это всего лишь один выстрел, и не слишком сложно выполнить задачу Rake, которую вы можете запустить по требованию.

Например, чтобы выбрать все записи статуса, разумно использовать что-то вродеследующее:

SELECT id FROM hero_statuses GROUP BY user_id, hero_type, recordable_id

Учитывая, что это достаточно уникальные записи в вашем наборе, вы можете удалить все те, которые вам не нужны:

DELETE FROM hero_statuses WHERE id NOT IN (SELECT id FROM hero_statuses GROUP BY user_id, hero_type, recordable_id)

Как и в любом другомоперация, которая включает DELETE FROM, я надеюсь, что вы не просто включите это в свои производственные данные без обычных мер предосторожности при резервном копировании.

Как предотвратить это в будущем, если это уникальные ограничения, создайте для них уникальный индекс:

add_index :hero_statuses, [ :user_id, :hero_type, :recordable_id ], :unique => true

Это создаст исключения ActiveRecord при попытке ввести дублирующую запись.Одним из преимуществ уникального индекса является то, что вы можете использовать функции «INSERT IGNORE INTO ...» или «INSERT ... ON DUPLICATE KEY ...» для восстановления от потенциальных дубликатов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...