Answered Delete Multiple Rows in Linq to SQL

  • Friday, February 29, 2008 5:00 PM
     
     
    I am trying to use linq to delete rows from SQL Server database.


    var something = (from inv in InventoryLTD
    where inv.rate == 1000
    select inv)


    I tried the following           

     foreach (var detail in something)
                 {
                     db.INVENTORYLTD.DeleteOnSubmit(detail);
                 }
    db.SubmitChanges();

    and


    db.INVENTORYLTD.DeleteAllOnSubmit(something);
    db.SubmitChanges();


    both of them only help me to delete one row from the table, but var something should have at least 100 rows .
    Can someone tell me why ?

    Thanks

Answers

  • Monday, March 03, 2008 8:42 PM
     
     Answered

     

    Well, It wasn't as easy as it seemed, but here it is,

     

    My "multiple delete" is called like this:

     

    Code Snippet

    private void btnDeleteSales_Click(object sender, EventArgs e)

    {

    using (MyLibraryDataContext dc = new MyLibraryDataContext())

    {

    dc.DeleteAll(dc.Books.Where(b => b.AuthorId == author.AuthorId &&

    b.Title.StartsWith("[SALE]")));

    }

    }

     

     

    Note that the "DeleteAll" function can receive any IQueryable, which should be obtained from the same DataContext where the DeleteAll is been invoked, but this is not defensively checked.

     

     

    An follwing is the extension method to the DataContext class, it uses some Regex to change:

    "SELECT .......... FROM table as t1 WHERE ............ t1.Field = ........   t1.Field2 > ........"

    into:

    "DELETE .......... FROM table WHERE ............ table.Field = ........   table.Field2 > ........"

    Note that some extra work is needed because "AS" is not allowed in the FROM clause on sql DELETE operations.

     

     

    Code Snippet

    using System;

    using System.Collections.Generic;

    using System.Linq;

    using System.Text;

    using System.Data.Linq;

    using System.Text.RegularExpressions;

    using System.Data.Common;

    namespace MyLibrary

    {

    public static class LinqExtensions

    {

    public static int DeleteAll(this DataContext dc, IQueryable collection)

    {

    DbCommand comm = dc.GetCommand(collection);

    Regex selectRE = new Regex("^SELECT[\\s]*(?<Fields>.*)[\\s]*FROM[\\s]*(?

    <Table>.*)[\\s]*AS[\\s]*(?<TableAlias>.*)[\\s]*WHERE[\\s]*(?<Condition>.*)", RegexOptions.IgnoreCase);

    Match m = selectRE.Match(comm.CommandText);

    if (!m.Success)

    throw new ArgumentException("Cannot delete this type of collection");

    string table = m.Groups["Table"].Value.Trim();

    string tableAlias = m.Groups["TableAlias"].Value.Trim();

    string condition = m.Groups["Condition"].Value.Trim().Replace(tableAlias,table);

     

    comm.CommandText = string.Format("DELETE FROM {0} WHERE {1}", table, condition);

    if (comm.Connection.State != System.Data.ConnectionState.Open)

    comm.Connection.Open();

    return comm.ExecuteNonQuery();

    }

    }

    }

     

     

 
 
Note that here any OrderBy, GroupBy and probably other Linq operators will produce SQL Errors, so they should be filtered too.

 

Of course it's not the most elegant way to do this, I suposse that it would be nicer to implement this by extending the Expression Visitor of the Linq to SQL Provider, adding a Delete() Extension method to the IQueryable interface, and when it's visited it produces a "DELETE" query, instead of "SELECT"... you get the idea :)

 

This approach could be extended to batch inserts and updates (maybe using Action<>), but i think those would require some "Lambda Expression to SQL" programming, don't they?.

 

any comments?

 

All Replies

  • Friday, February 29, 2008 5:36 PM
     
     

     

    Did you run SQL PRofiler to see what is being passed to the database?
  • Friday, February 29, 2008 6:58 PM
     
     

    Notice that performing a batch DELETE in this way, would make the 100 rows to be obtained from the db (with all the fields, except for the lazy loaded ones), and then a DELETE sql statement is auto-generated and sended back to the Sql for every row on SubmitChanges.

    Also, the whole collection must be present in memory (at the DataContext ChangeSet) at once.

     

    Depending on your scenario, this could be a big performance overhead.

    AFAIK Linq to SQL can't easily help you avoid this, that's why Linq still has Stored Procedure support, which seems to me as as a better choice here.

     

    Code Snippet

    db.DeleteInvByRate(100); // call a SP with a DELETE instruction

     

     

    Alternativily, a not so elegant solution, you can including the SQL statement in your code, as:

     

    Code Snippet

    db.ExecuteQuery("DELETE FROM ....");

     

     

    But of course!, I know this doesn't answer your question Wink

  • Friday, February 29, 2008 7:17 PM
     
     
    It only ran 1 delete sql.

    Also, I think it is caused by the primary key suggestion that you gave me.
    WHen I actually added one to it... it started to loop through it
  • Friday, February 29, 2008 7:18 PM
     
     
    So is there anyway to delete everything in one shot using linq?
  • Friday, February 29, 2008 7:25 PM
     
     

     

    The quick answer is: No without writing any SQL code (Stored Proc or ExecuteQuery)

     

  • Friday, February 29, 2008 7:29 PM
     
     
    Well... is there anyway to override the current delete method?  My company want everything in strong typed and they are trying to avoid SQL.
  • Friday, February 29, 2008 7:41 PM
     
     

     

    Yes, see here:

    http://weblogs.asp.net/scottgu/archive/2007/08/23/linq-to-sql-part-7-updating-our-database-using-stored-procedures.aspx

    BTW, in my opinion you should be using procs for any insert, updates and deletes. Do you guys (sorry ReneeC) have a DBA?

  • Friday, February 29, 2008 7:43 PM
     
     

     

    You can override the SubmitChanges, and write some custom code, that takes objects from the DataContext.GetChangeSet()

     

    But I think that your worse problem here is retrieving the whole collection from the db, keeping it in memory just to delete it. I don't you could avoid it simply. (In my very humble opinion, and without knowing details of the app you are working) my experience with Linq showed me that is a little naif to believe that you can use Linq to completely forget about SQL. It really helps you to forget about 99% of the tedious CRUD querys, or simple selects, but in some oportunities you will need SQL. The lack of strong typing won't hurt in a simple batch DELETE stored proc.

     

    Maybe someone else can think in a workaround, anyone?

  • Friday, February 29, 2008 7:47 PM
     
     
     crock wrote:

     

    Yes, see here:

    http://weblogs.asp.net/scottgu/archive/2007/08/23/linq-to-sql-part-7-updating-our-database-using-stored-procedures.aspx

    BTW, in my opinion you should be using procs for any insert, updates and deletes. Do you guys (sorry ReneeC) have a DBA?

     

    Yes you can override the Insert/Update/Delete by providing your own Stored Proc for it, but that's at a per-entity level, no batch operations.

     

    Linq (out-of-the-box) doesnt allow you to generate any INSERT/UPDATE/DELETE query except those auto-generated to persist single entity changes.

     

    There's no:

     

    dc.Orders.Where(price > 100).DeleteAll();

     

    producing a "DELETE FROM Orders WHERE price > 100" travelling to the Sql

     

    AFAIK at the moment that can only be acheived with SP, or SQL Queries,... or extending Linq to SQL Query Provider, (which is not an easy task! Smile)

  • Friday, February 29, 2008 8:49 PM
     
     

     

    Maybe that's an extension method we could write.
  • Saturday, March 01, 2008 12:34 AM
     
     

     

    Yes!  It would be fun surely Smile, I was thinking that you may need to extend the query provider, the object that translates the Expression Tree into SQL syntax, but maybe the Extension method could (in a not so elegant way as the Query Provider overriding) take the SQL select produced by the previous IQueryable, e.g:

     

    Code Snippet

    var query = dc.Orders.Where(o => o.Price > 100).DeleteAll();

     

     

     

    the extension method could be something like this:

     

    Code Snippet

    public void(or int) DeleteAll(this IQueryable collection) {

     

        // we should check that collection is an sql queryable attached to a DataContext

     

        string sql = collection.ToString(); // = "SELECT field1, field2 FROM Orders WHERE Price > 100"

     

        //edit this string, by replacing the "SELECT (anything) FROM" by a "DELETE"

       sql = sql.Replace( .... use Regex for this

       // sql = "DELETE FROM Orders WHERE Price > 100"

     

       DataContext dc = collection .... find the reference to DataContext, it must be in the QueryProvider object

        dc.ExecuteQuery(sql);

     

    }

     

     

     

    This is just pseudo-code... but it sounds like it should work, this is what you had in mind?

     

    Let's code it Smile!

     

  • Saturday, March 01, 2008 1:43 AM
     
     
    Looks good to me...
  • Monday, March 03, 2008 8:42 PM
     
     Answered

     

    Well, It wasn't as easy as it seemed, but here it is,

     

    My "multiple delete" is called like this:

     

    Code Snippet

    private void btnDeleteSales_Click(object sender, EventArgs e)

    {

    using (MyLibraryDataContext dc = new MyLibraryDataContext())

    {

    dc.DeleteAll(dc.Books.Where(b => b.AuthorId == author.AuthorId &&

    b.Title.StartsWith("[SALE]")));

    }

    }

     

     

    Note that the "DeleteAll" function can receive any IQueryable, which should be obtained from the same DataContext where the DeleteAll is been invoked, but this is not defensively checked.

     

     

    An follwing is the extension method to the DataContext class, it uses some Regex to change:

    "SELECT .......... FROM table as t1 WHERE ............ t1.Field = ........   t1.Field2 > ........"

    into:

    "DELETE .......... FROM table WHERE ............ table.Field = ........   table.Field2 > ........"

    Note that some extra work is needed because "AS" is not allowed in the FROM clause on sql DELETE operations.

     

     

    Code Snippet

    using System;

    using System.Collections.Generic;

    using System.Linq;

    using System.Text;

    using System.Data.Linq;

    using System.Text.RegularExpressions;

    using System.Data.Common;

    namespace MyLibrary

    {

    public static class LinqExtensions

    {

    public static int DeleteAll(this DataContext dc, IQueryable collection)

    {

    DbCommand comm = dc.GetCommand(collection);

    Regex selectRE = new Regex("^SELECT[\\s]*(?<Fields>.*)[\\s]*FROM[\\s]*(?

    <Table>.*)[\\s]*AS[\\s]*(?<TableAlias>.*)[\\s]*WHERE[\\s]*(?<Condition>.*)", RegexOptions.IgnoreCase);

    Match m = selectRE.Match(comm.CommandText);

    if (!m.Success)

    throw new ArgumentException("Cannot delete this type of collection");

    string table = m.Groups["Table"].Value.Trim();

    string tableAlias = m.Groups["TableAlias"].Value.Trim();

    string condition = m.Groups["Condition"].Value.Trim().Replace(tableAlias,table);

     

    comm.CommandText = string.Format("DELETE FROM {0} WHERE {1}", table, condition);

    if (comm.Connection.State != System.Data.ConnectionState.Open)

    comm.Connection.Open();

    return comm.ExecuteNonQuery();

    }

    }

    }

     

     

 
 
Note that here any OrderBy, GroupBy and probably other Linq operators will produce SQL Errors, so they should be filtered too.

 

Of course it's not the most elegant way to do this, I suposse that it would be nicer to implement this by extending the Expression Visitor of the Linq to SQL Provider, adding a Delete() Extension method to the IQueryable interface, and when it's visited it produces a "DELETE" query, instead of "SELECT"... you get the idea :)

 

This approach could be extended to batch inserts and updates (maybe using Action<>), but i think those would require some "Lambda Expression to SQL" programming, don't they?.

 

any comments?

 

  • Monday, March 03, 2008 9:49 PM
     
     
    That's very nice. 
    However, can I add it directly to the Datacontext class?

  • Monday, March 03, 2008 10:58 PM
     
     

    Well, you can't add to the DataContext base class, because you don't own it (unless you work on the ADO.Net Team ), but you can add it to a descendant class, your own MyDatabaseDataContext (you tipically would do this in a partial class)

     

    I wrote it as an extension method because it allows you to use it in any DataContext as long as you reference the extensions static class namespace, but both options are valid.

     

    Credits to Chris Rock for the idea Wink

  • Monday, March 03, 2008 11:26 PM
     
     

     

    Hey, I'll take credit for the idea.

    Ben, we should work more together in the future.

     

     

  • Friday, March 07, 2008 4:54 PM
     
     
    Benjaman,
        Can you explain where I should put the code in ?  Should I put it inside the Datacontext class?
    Please explain a little bit
  • Friday, March 07, 2008 6:15 PM
     
     

    Yes you can, you can add it using a partial class

    e.g:

    (in MyDataContext.cs)

    Code Snippet

      public partial class MyDataContext {

          

    public int DeleteAll(IQueryable collection) {

            .......

               }

      }

     

     

     

    But I would recommend you to put it in the an extension class as shown in my post above

     

    (in a file LinqExtensions.cs)

    Code Snippet

    namespace MyNamespace.Linq

    {

    public static class LinqExtensions

    {

    public static int DeleteAll(this DataContext dc, IQueryable collection)

    {

    }

    }

    }

     

     

     

    this extension file could be in your "Data.Linq.Util" assembly, allowing you to reuse code.

     

    the usage syntax would be the same, the difference is, if you put the method in your MyDataContext class you can only use it with a MyDataContext instance, if you write the extension class, you can use that method on any DataContext

     

    If one day you want to rewrite this DeleteAll method or the next Linq to SQL provides a better solution, you just need to modify this extension class, otherwise you would need to replace code in every DataContext you created (aka. Shotgun Surgery)

     

    Not sure if that was your doubt,
  • Monday, March 10, 2008 6:22 PM
     
     
    I tried to implement the code, but at this line :

    Regex selectRE = new Regex("^SELECT[\\s]*(?<Fields>.*)[\\s]*FROM[\\s]*(? <Table>.*)[\\s]*AS[\\s]*(?<TableAlias>.*)[\\s]*WHERE[\\s]*(?<Condition>.*)", RegexOptions.IgnoreCase);

    I got an error :

    parsing "^SELECT[\s]*(?<Fields>.*)[\s]*FROM[\s]*(? <Table>.*)[\s]*AS[\s]*(?<TableAlias>.*)[\s]*WHERE[\s]*(?<Condition>.*)" - Unrecognized grouping construct.


    can someone help?
  • Monday, March 10, 2008 7:08 PM
     
     
    I tried to fix the Regular Expression, but I can't make it pass the m.Success part .

    The Query that linq generage is :

    SELECT [t0].[GlJeDate], [t0].[RefEvId], [t0].[Fnid], [t0].[CBACid], [t0].[Snid], [t0].[STid], [t0].[SyId], [t0].[DealId], [t0].[DLTPid], [t0].[Cuid], [t0].[PositionDirection], [t0].[Qty], [t0].[TotalFuncCost], [t0].[FCAmountCost]
    FROM [dbo].[INVENTORYLTD] AS [t0]
    WHERE ([t0].[CBACid] = 10000675) AND ([t0].[Cuid] = 100002) AND ([t0].[DealId] = 10000090) AND ([t0].[DLTPid] = 10000011) AND (([t0].[Fnid]) = 10000042) AND ([t0].[GlJeDate] >= '2/1/2007 12:00:00 AM') AND ((UNICODE([t0].[PositionDirection])) = 76) AND ([t0].[RefEvId] = 10616305) AND ([t0].[Snid] = 10001491) AND ([t0].[Snid] = 10001491) AND ([t0].[STid] = 1000020) AND ([t0].[SyId] = 1000021)


    Benjaman, can you check this ?

  • Monday, March 10, 2008 7:54 PM
     
     

     

    Kenneth,

     

    when you copied the regular expression it seems that you accidentally inserted an space before "<Table"

     

    it should be:

     

    Code Snippet

    Regex selectRE = new Regex("^SELECT[\\s]*(?<Fields>.*)[\\s]*FROM[\\s]*(?<Table>.*)[\\s]*AS[\\s]*(?<TableAlias>.*)[\\s]*WHERE[\\s]*(?<Condition>.*)", RegexOptions.IgnoreCase);

     

     

  • Monday, March 10, 2008 9:09 PM
     
     
    It ends up with another problem... I have been debugging today.
    the DbCommand comm = this.GetCommand(collection);
    comm.commText always give me back @p0, @p1, @p2

    Is there anyway I can get the whole query instead of replacing each param?

    Also, how can I handle null value?

    Thanks
  • Monday, March 10, 2008 9:19 PM
     
     

    Kenneth,

     

     

    you dont need to replace those params on the CommandTest. The DbCommand you are obtaining already has this params set, and will replace them correctly when you execute it, it will handle nulls automatically too.

     

    Regards,

  • Monday, March 10, 2008 9:23 PM
     
     
    IT is true while you are executing the linq.
    However, in this situation, you are getting the text into the string condition.
    I have checked the delete statement that youar code passing into... it is sending :

    DELETE FROM [dbo].[INVENTORYLTD] WHERE ([dbo].[INVENTORYLTD].[CBACid] = @p0) AND ([dbo].[INVENTORYLTD].[Cuid] = @p1) AND ([dbo].[INVENTORYLTD].[DealId] = @p2) AND ([dbo].[INVENTORYLTD].[DLTPid] = @p3) AND (([dbo].[INVENTORYLTD].[Fnid]) = @p4) AND ([dbo].[INVENTORYLTD].[GlJeDate] >= @p5) AND ((CONVERT(NVarChar(1),[dbo].[INVENTORYLTD].[PositionDirection])) = @p6) AND ([dbo].[INVENTORYLTD].[RefEvId] = @p7) AND ([dbo].[INVENTORYLTD].[Snid] = @p8) AND ([dbo].[INVENTORYLTD].[STid] = @p9) AND ([dbo].[INVENTORYLTD].[SyId] = @p10)


    after running the exenonquery, my stuff is still in my db.
  • Monday, March 10, 2008 9:32 PM
     
     

     

    That command text is right, you wont get it with the values of the params replaced.

    But the DbCommand should have the values of that params loaded, can you watch that on a breakpoint?

    I tested it on a simple scenario and worked ok.

  • Monday, March 10, 2008 9:39 PM
     
     
    the delete statement is from the breakpoint at comm.CommandTetxt.
    It is the exact stuff that passing to the database
  • Monday, March 10, 2008 9:43 PM
     
     
    FYI
    This is the thing I am passing into the delete statement


                db.DeleteAll(db.INVENTORYLTD.Where(p => p.CBACid == toRemoveStrip.CBAcid
                    && p.Cuid == toRemoveStrip.Cuid && p.DealId == toRemoveStrip.DealId
                    && p.DLTPid == toRemoveStrip.DlTpid && p.Fnid == toRemoveStrip.Fnid
                    && p.GlJeDate >= toRemoveStrip.GlJeDate && p.PositionDirection.ToString() == toRemoveStrip.PositionDirection
                    && p.RefEvId == toRemoveStrip.RefEvId
                    && p.Snid == toRemoveStrip.SnId && p.STid == toRemoveStrip.Stid && p.SyId == toRemoveStrip.SyId));
  • Monday, March 10, 2008 9:47 PM
     
     

     

    Sorry but I couldn't reproduce it, Maybe this can help,

    It's a solution in which i used this with success:

     

    http://cid-2e532b202f3287ba.skydrive.live.com/self.aspx/P%c3%bablico/MSDN/MyLibrary/MyLibrarySln.zip

     

    In this zip is the VS2008 Solution, and a MyLibrary.bak file which is a backup of SQL Server Express db used.

    (Of course any db matching the dbml schema would be fine)

     

    It's strange that those params got lost Sad

  • Monday, March 10, 2008 9:50 PM
     
     
    I am guessing that you are passing to the function with a static field
    like DeleteAll (table.where(a == 10000))
    while I am passing a dyanmic field.
    Do yo uthink it is the case?
  • Monday, March 10, 2008 10:03 PM
     
     

    Hey, I didn't thought that could be a problem! It sounds posible! can u test hardcoding the parameters on your method (replacing them by constants)?

    If that way works, then we have a big limitation! Anyway, if passing dynamic parameters in to the linq query causes the problem, we can try to find a workaround Smile!.

     

    I'll try this on my samples tomorrow, Thanks!

  • Tuesday, March 11, 2008 12:11 PM
     
     

     

    Kenneth I couldn't get reproduce it that way either, I used dynamic parameters, obtained from different object properties, textboxs, etc., and when I call the GetCommand() method I always get a DbCommand with the parameters loaded (comm.Parameters collection) with their correct values assigned.

     

    In your DbCommand, the parameter list is empty?, or the parameters are there but have no values?

     

  • Tuesday, April 15, 2008 7:16 AM
     
     

    Hi all.  I tihnk I have an implementation that might suit your needs.  See my blog post at:

     

    [UPDATE]: My clipboard for my URL had someone else's url that I was browsing and I didnt' notice.  My article can be found here:

     

    http://www.aneyfamily.com/terryandann/post/2008/04/Batch-Updates-and-Deletes-with-LINQ-to-SQL.aspx

  • Friday, January 28, 2011 12:31 AM
     
     



    Thanks for your analysis! It's helpful to me, It's quite useful.

  • Microsoft is conducting an online survey to understand your opinion of the Msdn Web site. If you choose to participate, the online survey will be presented to you when you leave the Msdn Web site.

    Would you like to participate?