一尘不染

推荐的方法用Castle ActiveRecord插入许多行并忽略任何重复项

sql

我有一个Web方法,可将一堆食谱插入数据库的队列中(以存储用户对烹饪感兴趣的食谱,类似于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模式,并需要新的DB代码,这通常较难维护且实现成本更高。
  • 3)在每个循环之后创建并刷新。基本上,不要在单个事务中运行整个循环。提交添加的每一行,并捕获SQL错误并忽略。优点:启动成本低,并且不需要新的SQL后端代码。缺点:一次向数据库中插入大量行的速度可能会变慢,尽管用户是否会一次提交十几个新配方令人怀疑。

Castle或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();
   }
}

阅读 128

收藏
2021-05-23

共1个答案

一尘不染

您可以使用一条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;

一次插入少数几个的解决方案

迈克(Mike)评论说,临时表对于仅几条记录来说是一个过大的杀伤力。

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;
2021-05-23