SELECT * FROM X WHERE ID IN (…) mit Dapper ORM

230

Was ist der beste Weg, um eine Abfrage mit IN-Klausel mit Dapper ORM zu schreiben, wenn die Liste der Werte für die IN-Klausel aus der Geschäftslogik stammt? Angenommen, ich habe eine Abfrage:

SELECT * 
  FROM SomeTable 
 WHERE id IN (commaSeparatedListOfIDs)

Das commaSeparatedListOfIDswird von der Geschäftslogik übergeben und kann jede Art von sein IEnumerable(of Integer). Wie würde ich in diesem Fall eine Abfrage erstellen? Muss ich das tun, was ich bisher getan habe, was im Grunde eine Verkettung von Zeichenfolgen ist, oder gibt es eine Art fortgeschrittene Parameterzuordnungstechnik, die mir nicht bekannt ist?

Marko
quelle

Antworten:

364

Dapper unterstützt dies direkt. Beispielsweise...

string sql = "SELECT * FROM SomeTable WHERE id IN @ids"
var results = conn.Query(sql, new { ids = new[] { 1, 2, 3, 4, 5 }});
LukeH
quelle
46
Ich denke, es ist wichtig zu beachten, dass die Anzahl der Elemente, die Sie in Ihrem Array senden können, begrenzt ist. Ich habe das auf die harte Tour gemerkt, als ich zu viele Ausweise eingegeben habe. Ich erinnere mich nicht an die genaue Anzahl, aber aus meinem Gedächtnis sind es 200 Elemente, bevor Dapper aufhört, die Abfrage zu bearbeiten / auszuführen.
Marko
7
Marko, das ist wichtig. Wenn Sie dies auf diese Weise tun, können Sie eine andere Möglichkeit zum Abfragen Ihrer Daten in Betracht ziehen, z. B. einen Join oder einen Anti-Join, anstatt eine Liste mit IDs zu übergeben. Die IN-Klausel ist nicht die leistungsstärkste Abfrage und kann häufig durch eine vorhandene Klausel ersetzt werden, die schneller ist.
Don Rolling
24
Zu Ihrer Information - SQL Server 2008 R2 hat ein Limit von 2100 Einträgen in der INKlausel.
Jesse
6
Und SQLite hat ein Standardlimit von 999 Variablen.
Cameron
8
Achtung: In SQL Server schlägt dies fehl, wenn Sie mehrere Elemente in Ihrem Array haben und den Parameter in Klammern setzen. Durch Entfernen der Klammern wird das Problem behoben.
Ajbeaven
66

Direkt von der GitHub-Projekthomepage :

Mit Dapper können Sie IEnumerable übergeben und Ihre Abfrage automatisch parametrisieren.

connection.Query<int>(
    @"select * 
      from (select 1 as Id union all select 2 union all select 3) as X 
      where Id in @Ids", 
    new { Ids = new int[] { 1, 2, 3 });

Wird übersetzt in:

select * 
from (select 1 as Id union all select 2 union all select 3) as X 
where Id in (@Ids1, @Ids2, @Ids3)

// @Ids1 = 1 , @Ids2 = 2 , @Ids2 = 3
Faktor Mystic
quelle
43

Wenn Ihre INKlausel für MSSQL zu groß ist, können Sie ganz einfach einen TableValueParameter mit Dapper verwenden.

  1. Erstellen Sie Ihren TVP-Typ in MSSQL:

    CREATE TYPE [dbo].[MyTVP] AS TABLE([ProviderId] [int] NOT NULL)
  2. Erstellen Sie eine DataTablemit denselben Spalten wie dem TVP und füllen Sie sie mit Werten

    var tvpTable = new DataTable();
    tvpTable.Columns.Add(new DataColumn("ProviderId", typeof(int)));
    // fill the data table however you wish
  3. Ändern Sie Ihre Dapper-Abfrage, um eine INNER JOINin der TVP-Tabelle auszuführen:

    var query = @"SELECT * FROM Providers P
        INNER JOIN @tvp t ON p.ProviderId = t.ProviderId";
  4. Übergeben Sie die DataTable in Ihrem Dapper-Abfrageaufruf

    sqlConn.Query(query, new {tvp = tvpTable.AsTableValuedParameter("dbo.MyTVP")});

Dies funktioniert auch fantastisch, wenn Sie eine Massenaktualisierung mehrerer Spalten durchführen möchten - erstellen Sie einfach einen TVP und führen Sie UPDATEeinen inneren Join mit dem TVP durch.

Herr T.
quelle
Eine großartige Lösung funktioniert jedoch nicht mit .Net Core. Siehe folgende Frage: stackoverflow.com/questions/41132350/… . Siehe auch diese Seite: github.com/StackExchange/Dapper/issues/603
pcdev
3
Sie können auch machen betrachten sein , da dies nur ein Leistungsproblem für uns gelöst (die Werte wurden wir keine Duplikate enthalten passing). ProviderIdMyTVPPRIMARY KEY CLUSTERED
Richardissimo
@Richardissimo Kannst du ein Beispiel dafür zeigen? Ich kann die Syntax nicht richtig verstehen.
Mike Cole
14

Hier ist möglicherweise der schnellste Weg, um eine große Anzahl von Zeilen mit Dapper mithilfe einer Liste von IDs abzufragen. Ich verspreche Ihnen, dass dies schneller ist als fast jede andere Art, die Sie sich vorstellen können (mit der möglichen Ausnahme, dass Sie einen TVP verwenden, wie in einer anderen Antwort angegeben, und den ich nicht getestet habe, aber ich vermute, dass er langsamer ist, weil Sie noch füllen müssen der TVP). Es ist Planeten schneller als Dapper mit INSyntax und Universen schneller als Entity Framework Zeile für Zeile. Und es ist sogar Kontinente schneller als das Übergeben einer Liste von VALUESoder UNION ALL SELECTGegenständen. Es kann leicht erweitert werden, um einen mehrspaltigen Schlüssel zu verwenden. Fügen Sie einfach die zusätzlichen Spalten zu den DataTableBedingungen, der temporären Tabelle und den Verknüpfungsbedingungen hinzu.

public IReadOnlyCollection<Item> GetItemsByItemIds(IEnumerable<int> items) {
   var itemList = new HashSet(items);
   if (itemList.Count == 0) { return Enumerable.Empty<Item>().ToList().AsReadOnly(); }

   var itemDataTable = new DataTable();
   itemDataTable.Columns.Add("ItemId", typeof(int));
   itemList.ForEach(itemid => itemDataTable.Rows.Add(itemid));

   using (SqlConnection conn = GetConnection()) // however you get a connection
   using (var transaction = conn.BeginTransaction()) {
      conn.Execute(
         "CREATE TABLE #Items (ItemId int NOT NULL PRIMARY KEY CLUSTERED);",
         transaction: transaction
      );

      new SqlBulkCopy(conn, SqlBulkCopyOptions.Default, transaction) {
         DestinationTableName = "#Items",
         BulkCopyTimeout = 3600 // ridiculously large
      }
         .WriteToServer(itemDataTable);
      var result = conn
         .Query<Item>(@"
            SELECT i.ItemId, i.ItemName
            FROM #Items x INNER JOIN dbo.Items i ON x.ItemId = i.ItemId
            DROP TABLE #Items;",
            transaction: transaction,
            commandTimeout: 3600
         )
         .ToList()
         .AsReadOnly();
      transaction.Rollback(); // Or commit if you like
      return result;
   }
}

Beachten Sie, dass Sie ein wenig über Bulk Inserts lernen müssen. Es gibt Optionen zum Auslösen von Triggern (die Standardeinstellung ist Nein), zum Beachten von Einschränkungen, zum Sperren der Tabelle, zum Zulassen gleichzeitiger Einfügungen usw.

ErikE
quelle
Ja, ich stimme Ihrer allgemeinen Idee zu, eine temporäre Tabelle mit IDs zu erstellen und diese dann innerlich zu verbinden. Wir haben dies intern durchgeführt und die Abfrageleistung drastisch verbessert. Ich bin nicht sicher, ob ich die DataTable-Klasse für irgendetwas verwenden würde, aber Ihre Lösung ist vollständig gültig. Dies ist ein viel schnellerer Weg.
Marko
Das DataTablewird für den Bulk-Einsatz benötigt. Wie füge ich 50.000 Werte in die temporäre Tabelle ein?
ErikE
1
In Stücken von 1000, wenn ich mich richtig an das Limit erinnere? Wie auch immer, ich wusste nicht, dass Sie das Limit mit DataTable umgehen können, also habe ich heute etwas Neues gelernt ...
Marko
1
Das ist eine lächerliche Menge an Arbeit, wenn Sie stattdessen einen Tabellenwertparameter verwenden könnten. Dapper unterstützt sauber die Übergabe einer DataTable als TVP, wodurch Sie auf die Erstellung und Zerstörung einer temporären Tabelle verzichten und diese temporäre Tabelle über BulkCopy füllen können. Wir verwenden die TVP-basierte Lösung routinemäßig in Fällen, in denen die Anzahl der Parameter für die IN-Klausel zu groß wäre.
Herr T
3
Dies ist keine lächerliche Menge an Arbeit, besonders wenn man sie mit einer Hilfsklasse oder einer Erweiterungsmethode ein wenig abstrahiert.
ErikE
11

Stellen Sie außerdem sicher, dass Sie Ihre Abfragezeichenfolge nicht wie folgt in Klammern setzen:

SELECT Name from [USER] WHERE [UserId] in (@ids)

Ich hatte diese Ursache einen SQL-Syntaxfehler mit Dapper 1.50.2, der durch Entfernen von Klammern behoben wurde

SELECT Name from [USER] WHERE [UserId] in @ids
Brian Ogden
quelle
7

Es ist nicht erforderlich , ()die WHERE-Klausel wie in einem normalen SQL hinzuzufügen . Weil Dapper das automatisch für uns erledigt. Hier ist das syntax: -

const string SQL = "SELECT IntegerColumn, StringColumn FROM SomeTable WHERE IntegerColumn IN @listOfIntegers";

var conditions = new { listOfIntegers };

var results = connection.Query(SQL, conditions);
Codierer Absolut
quelle
6

Beispiel für Postgres:

string sql = "SELECT * FROM SomeTable WHERE id = ANY(@ids)"
var results = conn.Query(sql, new { ids = new[] { 1, 2, 3, 4, 5 }});
SanŚ́́́́Ý́́́́Ś́́́́
quelle
3

In meinem Fall habe ich Folgendes verwendet:

var query = "select * from table where Id IN @Ids";
var result = conn.Query<MyEntity>(query, new { Ids = ids });

Meine Variable "ids" in der zweiten Zeile ist eine IE-Zahl von Zeichenfolgen. Sie können auch Ganzzahlen sein, denke ich.

Cesar
quelle
List<string>?
Kiquenet
2

Nach meiner Erfahrung besteht die freundlichste Art, damit umzugehen, darin, eine Funktion zu haben, die eine Zeichenfolge in eine Wertetabelle konvertiert.

Es gibt viele Splitter-Funktionen im Web, Sie werden leicht eine für was auch immer finden, wenn Sie SQL-Geschmack haben.

Sie können dann tun ...

SELECT * FROM table WHERE id IN (SELECT id FROM split(@list_of_ids))

Oder

SELECT * FROM table INNER JOIN (SELECT id FROM split(@list_of_ids)) AS list ON list.id = table.id

(O.ä)

MatBailie
quelle