Рекомендуемый подход для вставки множества строк с помощью Castle ActiveRecord и игнорирования любых ошибок - PullRequest
7 голосов
/ 22 ноября 2011

У меня есть веб-метод, который вставляет кучу рецептов в очередь в базе данных (для хранения рецептов, которые пользователь заинтересован в приготовлении, аналогично очереди фильмов в NetFlix). Пользователь может проверить сразу несколько рецептов и поставить их в очередь. У меня есть код, подобный этому:

[WebMethod]
public void EnqueueRecipes(SecurityCredentials credentials, Guid[] recipeIds)
{
    DB.User user = new DB.User(credentials);

    using (new TransactionScope(OnDispose.Commit))
    {
       foreach (Guid rid in recipeIds)
       {
          DB.QueuedRecipe qr = new DB.QueuedRecipe(Guid.NewGuid(), user, new DB.Recipe(rid));
          qr.Create();
       }
    }
}

У меня есть уникальное ограничение на UserId / RecipeId, поэтому пользователь может поставить рецепт в очередь только один раз. Однако, если они выбирают рецепт, который уже находится в их очереди, я не хочу беспокоить пользователя сообщением об ошибке, я просто хочу проигнорировать этот рецепт.

Приведенный выше код вызовет исключение SQL, если ограничение уникальности нарушено. Какой лучший способ обойти это, и просто игнорировать дублирующиеся строки. Мои текущие идеи:

  • 1) Сначала загрузите всю очередь пользователя из базы данных и проверьте этот список первым. Если рецепт уже существует, просто continue в цикл for. Плюсы: никакие ненужные вставки SQL не отправляются на база данных. Минусы: медленнее, особенно если у пользователя большая очередь.
  • 2) Не используйте ActiveRecord и передайте весь массив recipeIds в функцию SQL. Эта функция проверит, существует ли каждая строка первый. Плюсы: Потенциально быстро, позволяет SQL обрабатывать всю грязную работу. Минусы: ломает шаблон ActiveRecord и требует нового кода БД, который часто сложнее поддерживать и дороже в реализации.
  • 3) CreateAndFlush после каждого цикла. По сути, не запускайте все это цикл в одной транзакции. Зафиксируйте каждый ряд, как он добавлен и ловить ошибки SQL и игнорировать. Плюсы: низкая стоимость запуска и не требуется новый код SQL сервера. Минусы: потенциально медленнее для вставки много строк в базе данных одновременно, хотя это сомнительно для пользователя когда-нибудь подаст более десятка новых рецептов одновременно.

Есть ли другие маленькие хитрости с Каслом или фреймворком NHibernate? Кроме того, моим SQL-сервером является PostgreSQL 9.0. Спасибо!

Обновление:

Я сделал снимок при первом заходе на посадку, и, похоже, он работает довольно хорошо. Мне пришло в голову, что мне не нужно загружать всю очередь, только те, которые появляются в recipeIds. Я считаю, что мой цикл foreach() теперь равен O (n ^ 2) в зависимости от эффективности List<Guid>::Contains(), но я думаю, что это, вероятно, прилично для размеров, с которыми я буду работать.

//Check for dupes
DB.QueuedRecipe[] dbRecipes = DB.QueuedRecipe.FindAll(Expression.In("Recipe",
   (from r in recipeIds select new DB.Recipe(r)).ToArray()
));

List<Guid> existing = (from r in dbRecipes select r.Recipe.RecipeId).ToList();

using (new TransactionScope(OnDispose.Commit))
{
   foreach (Guid rid in recipeIds)
   {
      if (existing.Contains(rid))
         continue;

      DB.QueuedRecipe qr = new DB.QueuedRecipe(Guid.NewGuid(), user, new DB.Recipe(rid));
      qr.Create();
   }
}

1 Ответ

5 голосов
/ 22 ноября 2011

Вы можете сделать это с помощью одного оператора SQL:

INSERT INTO user_recipe
SELECT new_UserId, new_RecipeId
FROM   user_recipe
WHERE  NOT EXISTS (
   SELECT *
   FROM   user_recipe
   WHERE  (UserId, RecipeId) = (new_UserId, new_RecipeId)
   );

SELECT возвращает строку, только если она еще не существует, поэтому она будет вставлена ​​только в этом случае.


Раствор для объемных вставок

Если у вас есть длинный список рецептов для вставки сразу, вы можете:

CREATE TEMP TABLE i(userId int, recipeid int) ON COMMIT DROP;

INSERT INTO i VALUES
(1,2), (2,4), (2,4), (2,7), (2,43), (23,113), (223,133);

INSERT INTO user_recipe
SELECT DISTINCT i.*  -- remove dupes from the insert candidates themselves
FROM   i
LEFT   JOIN user_recipe u USING (userid, recipeid)
WHERE  u.userid IS NULL;

Решение для вставки горстки за раз

Временная таблица была бы излишней для нескольких записей, как прокомментировал Майк.

INSERT INTO user_recipe
SELECT i.* 
FROM  (
    SELECT DISTINCT *     -- only if you need to remove possible dupes
    FROM (
       VALUES (1::int, 2::int)
          ,(2, 3)
          ,(2, 4)
          ,(2, 4)            -- dupe will be removed
          ,(2, 43)
          ,(23, 113)
          ,(223, 133)
       ) i(userid, recipeid)
    ) i
LEFT   JOIN user_recipe u USING (userid, recipeid)
WHERE  u.userid IS NULL;
...