建议使用Castle ActiveRecord插入许多行并忽略任何欺骗行

我有一个webmethod,它将一堆食谱插入到数据库的队列中(以存储用户有兴趣烹饪的食谱,类似于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有一个独特的约束,因此用户只能将配方排入一次。 但是,如果他们碰巧选择了已经在队列中的配方,我真的不想用错误消息打扰用户,我只想忽略该配方。

如果违反了唯一约束,上面的代码将抛出SQLexception。 解决这个问题的最佳方法是什么,只需忽略重复的行。 我目前的想法是:

  • 1)首先从数据库加载用户的整个队列,然后首先检查该列表。 如果配方已存在,只需continue执行for循环。 优点:没有不必要的SQL插入发送到数据库。 缺点:较慢,特别是如果用户有一个大队列。
  • 2)不要使用ActiveRecord,而是将整个recipeIds数组传递给SQL函数。 此函数将检查每行是否首先存在。 优点:潜在的速度,让SQL处理所有肮脏的工作。 缺点:打破ActiveRecord模式并需要新的DB代码,这通常难以维护并且实现成本更高。
  • 3)每次循环后CreateAndFlush。 基本上,不要在单个事务中运行整个循环。 在添加时提交每一行并捕获SQL错误并忽略。 优点:启动成本低,不需要新的SQL后端代码。 缺点:一次将大量行插入数据库的速度可能会慢一些,尽管用户一次会提交十几个新配方是值得怀疑的。

还有Castle或NHibernate框架的其他小技巧吗? 另外,我的SQL后端是PostgreSQL 9.0。 谢谢!

更新:

我在第一种方法中拍了一下它似乎工作得很好。 它发生在我身上我不必加载整个队列,只需加载出来在recipeIds中的队列。 我相信我的foreach()循环现在是O(n ^ 2),这取决于List::Contains()的效率,但我认为这对于我将要使用的大小来说可能是不错的。

 //Check for dupes DB.QueuedRecipe[] dbRecipes = DB.QueuedRecipe.FindAll(Expression.In("Recipe", (from r in recipeIds select new DB.Recipe(r)).ToArray() )); List 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(); } } 

您可以使用单个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;