2
votes

Comment faire une jointure LINQ qui se comporte exactement comme une jointure interne de base de données physique?

MODIFIER : Ma question initiale était confuse et ambiguë, alors permettez-moi de recommencer.

La source de données est une collection de fichiers CSV, il n'y a donc pas de base de données réelle. Il s'agit d'une intégration avec un système hérité vieux de plusieurs décennies basé au Japon.

J'ai une fonction ac # qui doit prendre 2 DataTables et 2 noms de colonnes comme paramètres . Ma fonction doit faire l'équivalent d'un INNER JOIN sur ces 2 datatables, puis retourner toutes les colonnes de la première table, et uniquement la "colonne jointe" de la 2ème table. Les schémas (lire: colonnes) de ces tables de données ne seront pas connus avant l'exécution, donc la fonction ne peut pas avoir de noms de colonnes codés en dur. Ma fonction doit enfin renvoyer un nouveau DataTable avec les données jointes en interne, et un ensemble de résultats DISTINCT basé sur la liste de sélection comme juste spécifié.

Voici ma tentative [modifiée], qui semble produire un ensemble de résultats prometteurs:

Common.JoinDataTables2(dtCSV, _dtModelOptions, "CMODEL", "ModelID");

Je l'appelle comme ceci:

public static DataTable JoinDataTables2(DataTable dt1, DataTable dt2, string table1KeyField, string table2KeyField) {
   DataTable result = ( from dataRows1 in dt1.AsEnumerable()
                        join dataRows2 in dt2.AsEnumerable()
                        on dataRows1.Field<string>(table1KeyField) equals dataRows2.Field<string>(table2KeyField)
                        select dataRows1).CopyToDataTable();
   return result;
}

Mon objectif est d'effectuer une jointure interne comme dans une base de données physique, avec un jeu de résultats distinct basé sur le jeu de résultats spécifié ci-dessus. Vous vous demandez peut-être pourquoi je ne fais pas simplement la jointure dans la base de données. C'est parce qu'il n'y a pas de base de données; les données proviennent de fichiers CSV générés à partir d'un système tiers.

J'ai donc 3 problèmes restants:

  1. Je ne sais pas si l'ensemble de résultats que je récupère est correct en fonction du comportement INNER JOIN.
  2. La liste de sélection n'inclut pas la "colonne de jointure" pour le 2ème datatable (dans cet exemple particulier, ce serait "ModelID"), et j'en ai besoin. Une fois cela fait, je peux confirmer que les valeurs CMODEL correspondent aux valeurs ModelID, et ainsi confirmer que j'ai une jointure valide. (Ce n'est qu'un scénario, ce sera différent, donc aucun nom de colonne ne peut être codé en dur dans la fonction.)
  3. Comment DISTINCTER le jeu de résultats?

Voici un exemple concret de mon système, mais encore une fois, les tables de données et les schémas seront tous différents:

Colonnes dtCSV :

  1. CMODEL
  2. CATT_CD
  3. NSTAND
  4. CAPPLY1
  5. CAPPLY2
  6. DREFIX_D

_dtModelOptions colonnes:

  1. SeriesID
  2. ModelID
  3. OptionID

De quels changements ai-je besoin pour ma fonction pour que:

  1. Il fait un INNER JOIN et un DISTINCT (le fait-il déjà?)
  2. Il sélectionne toutes les colonnes de la première table et uniquement la "colonne de jointure" de la deuxième table (actuellement, il ne reçoit que les colonnes de la première table)
  3. Les performances sont aussi rapides que possible (j’étais auparavant foreach à travers les enregistrements pour réaliser la jointure, et cette approche était terriblement lente.)

Merci pour tout conseil, j'apprécie vraiment le temps précieux de chacun.


8 commentaires

Essayez de supprimer de r dans lj.DefaultIfEmpty ()


Pour écrire une requête pour la jointure interne avec une condition, vous devez créer deux types anonymes (un pour la table de gauche et un pour la table de droite) en utilisant un nouveau mot-clé et comparer les deux types anonymes


@ChetanRanpariya qui est très prometteur - j'obtiens beaucoup moins de résultats maintenant - mais comment sélectionner toutes les colonnes du premier tableau et uniquement la "colonne jointe" du deuxième tableau? (Je ne connais pas les noms des colonnes avant l'exécution)


Quel est exactement le but de cette méthode? Vous joignez deux tables séparées et distinctes sur un champ commun, mais en sélectionnant les lignes de la première sans filtrage. Vous créez essentiellement un produit croisé des tables, mais vous supprimez les éléments de la deuxième table. Vos résultats ne contiendront que les lignes de la première table dupliquée.


Je ne suis pas sûr de l'avantage que vous tirez de cette opération par rapport à l'utilisation de linq sur les tables réelles. Vous semblez rendre les choses plus complexes pour moins d'avantages. Y a-t-il une raison pour laquelle je manque pour créer cette méthode?


Je veux juste joindre 2 datatables au moment de l'exécution (inconnu au moment de la conception), et sélectionner toutes les colonnes de la première table et juste la "colonne jointe" de la 2ème table. Tout comme une jointure interne contre une base de données réelle.


Il n'y a pas de tableaux réels. Les sources de données sont des fichiers CSV qui sont chargés dans c # DataTables.


Désolé, mais la requête linq sera toujours plus lente que la requête SQL, en raison de la façon dont ils accèdent aux données. Voulez-vous savoir comment y parvenir?


4 Réponses :


1
votes

Précédemment Soution ...

public static DataTable JoinDataTables2(DataTable dt1, DataTable dt2, string table1KeyField, string table2KeyField)
{
    DataTable joinTable = new DataTable();
    foreach (DataColumn dt1Column in dt1.Columns)
    {
        joinTable.Columns.Add(dt1Column.ColumnName, dt1Column.DataType);
    }

    var col2 = dt2.Columns[table2KeyField];
    joinTable.Columns.Add(col2.ColumnName,typeof(string));

    var result = (from dataRows1 in dt1.AsEnumerable()
                  join dataRows2 in dt2.AsEnumerable()
                      on dataRows1.Field<string>(table1KeyField) equals dataRows2.Field<string>(table2KeyField)
                  select new
                  {
                      Col1 = dataRows1,
                      Col2 = dataRows2.Field<string>(table2KeyField)
                  });
    foreach (var row in result)
    {
        DataRow dr = joinTable.NewRow();
        foreach (DataColumn dt1Column in dt1.Columns)
        {
            dr[dt1Column.ColumnName] = row.Col1[dt1Column.ColumnName];
        }

        dr[table2KeyField] = row.Col2;
        joinTable.Rows.Add(dr);
    }
    joinTable.AcceptChanges();
    return joinTable.AsEnumerable().Distinct().CopyToDataTable();
}

Vous pouvez lister toutes les colonnes de table1 dans la requête de sélection. La requête suivante a un DataTable par définition avec toutes les colonnes de table1 et uniquement la colonne clé de table2. Cela peut vous aider.

public static DataTable JoinDataTables2(DataTable dt1, DataTable dt2, string table1KeyField, string table2KeyField) {
   DataTable result = ( from dataRows1 in dt1.AsEnumerable()
                            join dataRows2 in dt2.AsEnumerable()
                            on dataRows1.Field<string>(table1KeyField) equals dataRows2.Field<string>(table2KeyField) 
                            select new {Col1= datarows1Field<string>(table1FieldName), Col2= datarows2.Field<string>(table2FieldName)}).Distinct().CopyToDataTable();
   return result;
}


9 commentaires

Sria, ce que je recherche, et comme mentionné dans mon article d'origine, c'est que je dois renvoyer TOUTES les colonnes de la première table de données, et uniquement la "colonne de jointure" de la 2ème table de données. Votre solution ne renvoie que 2 colonnes - 1 de chaque table. Pouvez-vous modifier votre solution?


@HerrimanCoder veuillez vérifier la version mise à jour. Cela peut fonctionner pour vous. Pour éviter de lister toutes les colonnes, j'ajoute des colonnes de table1 en boucle.


2 problèmes: (1) Le foreach () le rend très lent - je faisais quelque chose de similaire avant et je voulais m'éloigner de la lenteur; (2) la jointure effectue une sorte de jointure croisée et produit plus de 1M d'enregistrements au lieu de 100K d'enregistrements comme prévu.


Il ne s'agit pas d'une jointure croisée, mais si vous travaillez sur des enregistrements de 100k, je peux vous suggérer d'utiliser la jointure de base de données et non linq.


Comme mentionné dans l'article et les commentaires, il n'y a pas de base de données. La source de données est constituée de fichiers CSV d'un ancien système client. Si je pouvais rejoindre directement la base de données, je n'aurais pas eu besoin de poster cette question.


Oh oui. Désolé, j'ai raté ça. Avez-vous trouvé votre solution ou essayez-vous toujours de l'optimiser?


Toujours en attente d'une solution correcte et performante - pas de foreach.


c'est le moyen le plus rapide que j'ai trouvé jusqu'à présent. J'ai essayé 1 million d'enregistrements dans le tableau1 et un demi-million dans le tableau2. Il génère des enregistrements de jointure en 3 secondes.


Désolé pour le long retard, j'ai enfin pu tester et vérifier cela par rapport à des données réelles - fonctionne très bien !! Je vous remercie.



0
votes

[Mise à jour n ° 3]

  1. Je ne sais pas si l'ensemble de résultats que je récupère est correct en fonction du comportement INNER JOIN.

Un ensemble de résultats renvoyé par une requête linq représente exactement ce que vous avez écrit dans une requête.

  1. La liste de sélection n'inclut pas la "colonne de jointure" pour la deuxième table de données (dans> cet exemple particulier, qui serait "ModelID"), et je en a besoin.

La réponse est très simple: votre requête ne renvoie les données que du premier datatable (BTW: vous l'avez déjà mentionné dans la description du problème).

Une fois cela fait, je peux confirmer que les valeurs CMODEL correspond aux valeurs ModelID et confirme ainsi que j'ai une jointure valide. (Ce n'est qu'un scénario, ce sera différent, donc aucun nom de colonne ne peut être codé en dur dans la fonction.)

Vous pouvez être sûr qu'une requête Linq2DataSet renvoie les bons ID. Ils doivent correspondre pour pouvoir les rejoindre. S'il n'y a pas de correspondance, un ensemble de résultats sera vide! Il semble que vous deviez améliorer vos connaissances sur les jointures. Veuillez lire cet excellent article: Représentation visuelle des jointures SQL a>

Une version courte de l'article connexe:

jointure à gauche

DataTable resultDt = OleDbJoin("FullFileName1", "FullFileName2", "F1", "F2");

jointure interne

public static DataTable OleDbJoin(string csv1, string csv2, string key1, string key2)
{
    DataTable dt = new DataTable();

    string sConn = string.Format(@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0}\;Extended Properties='text;HDR=No;FMT=CSVDelimited()';", Path.GetDirectoryName(csv1));
    string sSql = string.Format(@"SELECT T.*
        FROM (
            SELECT * FROM [{0}] AS t1
            INNER JOIN (SELECT * FROM [{1}]) AS t2
                ON t1.[{2}] = t2.[{3}]) AS T;",
            Path.GetFileName(csv1), Path.GetFileName(csv2), key1, key2);

    try
    {
        using (OleDbConnection oConn = new OleDbConnection(sConn))
        {
            using (OleDbCommand oComm = new OleDbCommand(sSql, oConn))
            {
                oConn.Open();
                OleDbDataReader oRdr = oComm.ExecuteReader();
                dt.Load(oRdr);
                oComm.Dispose();
                oRdr.Dispose();
                oConn.Close();
                oConn.Dispose();
            }
        }
    }
    catch(OleDbException ex)
    {
        Console.WriteLine(ex.Message);
    }
    catch(Exception ex)
    {
        Console.WriteLine(ex.Message);
    }

    return dt;
}

jointure droite

select new {datarows1, datarows2}

jointure croisée

cross join returns the cartesian product of the sets
  1. Comment DISTINCTER le jeu de résultats?

Il existe un Méthode distincte .

Mais je ne suis pas sûr, vous en avez vraiment besoin; (

Remarque générale:

Il y a quelques façons de lire délimité files (* .csv):

1) en utilisant les méthodes de lecture de fichier texte "standard" et en divisant le texte en parties dans une ou plusieurs boucles [for]

Voir: Un lecteur CSV rapide

2) en utilisant les méthodes linq, c'est-à-dire: Select()

Remarque: La plupart des programmeurs savent bien que Les méthodes linq seront beaucoup plus lentes que la boucle [for] lorsque vous travaillez sur de grands ensembles de données.
Pour pouvoir projeter des champs à partir de tables jointes, vous devez utiliser:

Set1 = [1, 2, 3, 5]
Set2 = [2, 4, 5]
Resultset = [2,4,5] // gets [2] from right (set2), [4,5] are common (set1 and set2)

Si vous souhaitez créer des colonnes dynamiques à l'aide de Linq, consultez: Interroger une table de données avec des noms de colonnes dynamiques à l'aide de LINQ


Voici un code complet pour joindre deux tables de données en une seule Datatable : dotnetfiddle


3) avec OleDb: OleDbConnection , OleDbCommand

Voir:
Utilisation d'OleDb pour importer des fichiers texte onglet CSV personnalisé
Lire les colonnes spécifiques aux fichiers texte

Votre méthode d'extension pourrait ressembler à:

Set1 = [1, 2, 3, 5]
Set2 = [2, 4, 5]
Resultset = [2,5] //only common items (set1 and set2)

Appel:

Set1 = [1, 2, 3, 5]
Set2 = [2, 4, 5]
Resultset = [1,2,5] //get [1] from left (set1), [2,5] are common items (set1 and set2)

Conditions requises: em >
- les deux fichiers csv doivent être dans le même répertoire
- fichiers csv utilisant un séparateur standard pour les fichiers csv, ee: Fichier Schema.ini
- il n'y a pas d'en-tête dans les fichiers (pas de nom de colonne)


3 commentaires

Maciej, merci pour votre réponse très approfondie. L'une des pièces les plus importantes reste sans réponse: comment récupérer les colonnes de la 2ème datatable. Je crois qu'il doit y avoir un moyen, car cela peut être fait de manière déclarative au moment du code; il doit y avoir un moyen de le faire à l'exécution, avec un schéma inconnu jusqu'à l'exécution. Si vous pouvez répondre, je vous attribuerai la réponse. Exemple, je veux pouvoir passer un tableau de noms de colonnes dans la fonction, pour les valeurs de colonne que je veux dans le cadre de la table de données jointe. exemple: {"SalesID", "FirstName", "SomeCode"} - etc.


Le lien dans votre [Mise à jour] semble prometteur, mais comment fonctionnera-t-il dans mon exemple publié? Veuillez poster mon exemple avec les colonnes dynamiques. Merci.


@HerrimanCoder, j'ai à nouveau mis à jour ma réponse. Veuillez trouver un lien vers dotnetfiddle. Vous y trouverez un code complet sur la manière de joindre deux tables en une seule datatable. Veuillez également vérifier une méthode OleDbJoin . C'est très intéressant.



0
votes

Il y a un peu d'ambiguïté, mais d'après ce que j'ai compris, vous devez Join deux tables et obtenir une ligne des deux (ou moins) après avoir appliqué un Distinct () sur les résultats de la Join . Tout cela, étant donné que les colonnes ne sont pas prédéfinies.

Voici ma solution:

  1. Ajoutez une classe Result pour envelopper le résultat de votre Join

    public static DataTable JoinDataTables2(DataTable dt1, DataTable dt2, string table1KeyField, string table2KeyField)
    {
        // Join with final selection containing rows from both the tablles
        var query = from dataRows1 in dt1.AsEnumerable()
                    join dataRows2 in dt2.AsEnumerable()
                        on dataRows1.Field<string>(table1KeyField) equals dataRows2.Field<string>(table2KeyField)
                    select new Result
                    {
                        Table1Row = dataRows1,
                        Table2Row = dataRows2,
                        DistictFieldValue = dataRows2[table2KeyField].ToString() // This could be anything else, even passed as an argument to the method
                    };
    
        // Dictinct on the results above
        var queryWithDistictResults = query.Distinct(new ResultComparer());
    
        // Write your logic to convert the Results Collection to a single data table with whatever columns you want
        DataTable result = queryWithDistictResults // <= YOUR LOGIC HERE
    
        return result;
    }
    
  2. Ajoutez une classe ResultComparer pour vous aider avec votre propre logique pour obtenir des résultats Distinct ()

    public class ResultComparer : IEqualityComparer<Result>
    {
        public bool Equals(Result x, Result y)
        {
            // Your logic to get distinct elements
            return x.DistictFieldValue == y.DistictFieldValue;
        }
    
        public int GetHashCode(Result obj)
        {
            return 0; // To enforce the Equals() gets callled.
        }
    }
    
  3. Mettez à jour votre méthode pour utiliser les classes ci-dessus

    public class Result
    {
        public DataRow Table1Row { get; set; }
        public DataRow Table2Row { get; set; }
    
        public string DistictFieldValue { get; set; }
    }
    


4 commentaires

peu, cela semble très prometteur, mais je suis perplexe devant le résultat du retour, qui semble renvoyer 2 lignes + une valeur arbitraire. Je souhaite renvoyer toutes les lignes de la jointure et toutes les colonnes de la 1ère table + juste la colonne de jointure de la 2ème table. Votre solution fait-elle cela? Je veux l'essayer, mais d'abord si vous pouvez m'aider à comprendre. Merci.


Un dilemme que j'ai avec votre approche est que je me retrouve avec 2 DataRow indépendants et non liés pour chaque objet Result. Existe-t-il un moyen simple de les combiner en un seul flux de données et en un datatable? Si tel est le cas, je peux supprimer les colonnes que je ne veux pas et renvoyer la table de données finale.


Ils ne sont pas vraiment sans rapport, ils sont le résultat du JOIN. Pour joindre les 2 lignes, vous pouvez jeter un œil à ceci: stackoverflow.com/questions/16945569/...


Je dois éviter foreach es parce que je traite avec 1/2 million de lignes de données à chaque fois, et cela ralentit vraiment les choses. Lorsque je me suis éloigné de foreach et que je suis allé à linq, ma performance s'est remarquablement améliorée - je ne peux donc pas revenir aux boucles. N'y a-t-il pas un moyen d'avoir 1 jeu de flèches de données et de définir les colonnes souhaitées dans le new {} ?



0
votes

Si chaque fichier CSV représente une table de votre base de données, envisagez de faire quelque chose de similaire au framework d'entité.

Au lieu de IQueryable<...> laissez vos DbSets code> implémenter IEnumerable<...>

Si vous avez seulement besoin de récupérer les données, ce sera assez facile. Si vous souhaitez également mettre à jour, vous devrez implémenter (ou réutiliser) un DbChangeTracker

Students.SaveChanges()
Teachers.SaveChanges();
Schools.SaveChanges();

Vous aurez également besoin d'un DbContext qui contient tous vos DbSets:

void SaveChanges()
{
    this.ChangeTracker.SaveChanges();
}

Vous pouvez améliorer les performances en vous rappelant les éléments déjà récupérés. Mettez-les dans un dédicace, utilisez la clé primaire comme clé diconique. Ajoutez également une fonction Find au DbSet:

class DbChangeTracker<T, TKey> : IEnumerable<T> where T : class
{
     public IEqualityComparer<T> ValueComparer {get; set;}
     ...
}

C'est plus simple si chaque élément de table a le même type de clé primaire:

void SaveChanges()
{
    // your CsvFile database has functions to add / update / remove items
    foreach (var itemToAdd in itemsToAdd)
    {
        csvDatabase.Add(itemToAdd);
    }

    // update or remove fetched items with OriginalValue unequal to Value
    var itemsToUpdate = this.fetchedItems
        .Where(fetchedItem => !ValueComparer.Equals(fetchedItem.OriginalValue, fetchedItem.Value)
        .ToList();

    foreach (Entity<T> itemToUpdate in itemsToUpdate)
    {
        if (itemToUpdate.Value == null)
        {   // remove
            csvFile.Remove(itemToUpdate.OriginalValue);
        }
        else
        {   // update
            csvFile.Update(...);
        } 
    }
}

Sinon, vous devrez indiquer à votre DbSet le type de clé primaire:

public Entity<T> Find(TKey primaryKey)
{
    // is it already in the Dictionary (found before)?
    // if not: get it from the CsvDatabase and put it in the dictionary
    if (!fetchedItems.TryGetValue(primaryKey, out Entity<T> fetchedEntity))
    {
        // not fetched yet, fetch if from your Csv File
        T fetchedItem = ...
        // what to do if does not exist?
        // add to the dictionary:
        fetchedEntities.Add(new Entity<T>
        {
            value = fetchedItem,
            originalValue = (T)fetchedItem.Clone(),
            // so if value changes, original does not change
        });
    }
    return fetchedItem;
}

Si vous décidez de conserver vos éléments dans un dictionnaire, puis laissez votre GetEnumerator renvoyer d'abord les éléments déjà récupérés, avant de récupérer les nouvelles lignes de votre fichier CSV.

Ajouter / Mettre à jour / Supprimer des éléments

Pour cela, vous devez pouvoir ajouter / mettre à jour / supprimer des éléments de votre fichier CsV. Je suppose qu'il existe déjà des fonctions pour cela.

Pour faire la mise à jour efficacement, vous aurez besoin de quelque chose de similaire à DbContext.SaveChanges. Laissez chaque DbSet se souvenir des éléments à ajouter / supprimer / mettre à jour, en utilisant un ChangeTracker:

class Entity<T> where T : IPrimaryKey
{
    public T Value {get; set;}
    public T OriginalValue {get; set;}
}

class ChangeTracker<T, TKey> where T: ICloneable
{
    readonly Dictionary<int, Entity<T, TKey>> fetchedEntities = new Dictionary<int, Entity<T, TKey>>
    readonly List<T> itemsToAdd = new List<T>();

    public T Add(T item)
    {
        // TODO: check for not NULL, and Id == 0
        this.ItemsToAdd.Add(itemToAdd);
        return item;
    }
    public void Remove(T item)
    {
        // TODO: check not null, and primary key != 0
        Entity<T> entityToRemove = Find(item.Id);
        // TODO: decide what to do if there is no such item
        entityToRemove.Value = null;
        // null indicates it is about to be removed
    }

Vous aurez besoin d'un Find qui se souvient de la valeur d'origine:

class DbSet<T, TKey> : IEnumerable<T> where T : class
{
     private readonly Dictinary<TKey, T> fetchedItems = ...
}

Enfin vos SaveChanges ()

interface IPrimaryKey
{
     int Id {get;}
}

class DbSet<T> : IEnumerable<T> where T : IPrimaryKey {...}

Apparemment, si vous voulez pouvoir mettre à jour des éléments de votre base de données, vous devez être en mesure de vérifier si les éléments ont changé. Vous aurez besoin d'un IEqualityComparer qui vérifie par valeur

class DbSet<T> : IEnumerable<T>
{
    private readonly Dictionary<int, T> fetchedItems = new Dictionary<int, T>();

    public T Find(int id)
    {
        if (!fetchedItems.TryGetValue(id, out T fetchedItem))
        {
            // fetch elements using ReadCsvFile and put them in the Dictionary
            // until you found the item with the requested primary key
            // or until the end of your sequence
        }
        return fetchedItem;
    }
}

DbSet SaveChanges:

class DbContext
{
      public DbSet<School> Schools {get; } = new DbSet<School>{CsvFile = ...};
      public DbSet<Teacher> Teachers {get; } = new DbSet<Teacher> {CsvFile = ...};
      public DbSet<Student> Students {get; } = new DbSet<Student> {CsvFile = ...};
}

Modifications de sauvegarde DbContext:

public DbSet<T> : IEnumerable<T> where T: class
{
    public FileInfo CsvFile {get; set;}

    public IEnumerator<T> GetEnumerator()
    {
        return this.ReadCsvFile().GetEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    protected IEnumerable<T> ReadCsvFile()
    {
        // open the CsvFile, read the lines and convert to objects of type T
        // consider using Nuget package CsvHelper
        ...
        foreach (var csvLine in csvLines)
        {
            T item = Create<T>(csvLine); // TODO: write how to convert a line into T
            yield return T;
        }
    }
}


1 commentaires

La lecture des données CSV dans une table de données est déjà rapide et facile; pas le problème que j'essaie de résoudre. Mais merci quand même pour votre réponse réfléchie. Je recherche juste un moyen élégant de renvoyer des colonnes connues uniquement au moment de l'exécution résultant d'une jointure LINQ de 2 tables de données.