【问题标题】:How to do a LINQ join that behaves exactly like a physical database inner join?如何进行行为与物理数据库内部连接完全相同的 LINQ 连接?
【发布时间】:2019-01-08 00:51:54
【问题描述】:

编辑:我最初的问题令人困惑和模棱两可,所以让我重新开始。

数据源是 CSV 文件的集合,因此没有实际的数据库。这是与日本已有数十年历史的旧系统的集成。

我有一个 c# 函数需要将 2 个DataTables 和 2 个列名作为参数。我的函数需要对这两个数据表执行相当于INNER JOIN 的操作,然后返回第一个表中的所有列,并且只返回第二个表中的“连接列”。这些数据表的模式(读取:列)直到运行时才知道,因此该函数不能有任何硬编码的列名。我的函数最后需要返回一个包含内部连接数据的新 DataTable,以及一个基于刚刚指定的选择列表的 DISTINCTed 结果集。

这是我的 [修改] 尝试,它似乎产生了一个有希望的结果集:

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;
}

我这样称呼它:

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

我的目标是像在物理数据库中一样执行内部联接,并根据上面指定的结果集使用不同的结果集。 您可能想知道为什么我不只是简单地进行联接在数据库中。这是因为没有数据库;数据来自第三方系统生成的 CSV 文件。

所以我还有 3 个问题:

  1. 根据 INNER JOIN 行为,我不确定我返回的结果集是否正确。
  2. 选择列表不包括第二个数据表的“连接列”(在此特定示例中为“ModelID”),我需要它。一旦这样做,我可以确认 CMODEL 值与 ModelID 值匹配,从而确认我有一个有效的连接。 (这只是一种情况,它会有所不同,因此不能在函数中硬编码列名。)
  3. 如何区分结果集?

这是我系统中的一个具体示例,但同样,数据表和架构都会有所不同:

dtCSV 列:

  1. CMODEL
  2. CATT_CD
  3. 不支持
  4. CAPPLY1
  5. CAPPLY2
  6. DREFIX_D

_dtModelOptions 列:

  1. 系列ID
  2. 型号ID
  3. 选项ID

我需要对我的功能进行哪些更改,以便:

  1. 它执行 INNER JOIN 和 DISTINCT(它已经这样做了吗?)
  2. 它从第一个表中选择所有列,并且只从第二个表中选择“join-column”(目前它只获取第一个表的列)
  3. 性能尽可能快(我之前foreaching 通过记录来实现连接,但这种方法非常慢。)

感谢您的建议,非常感谢大家的宝贵时间。

【问题讨论】:

  • 尝试删除from r in lj.DefaultIfEmpty()
  • 要使用条件编写内部连接查询,您需要使用 new 关键字创建两种匿名类型(一种用于左表,一种用于右表)并比较两种匿名类型
  • @ChetanRanpariya 这是非常有前途的——我现在得到的结果要少得多——但是如何从第一个表中选择所有列,而从第二个表中只选择“连接列”? (直到运行时我才知道列名)
  • 这个方法的目标到底是什么?您要在一个公共字段上连接两个单独且不同的表,但从第一个表中选择行而不进行过滤。您实际上是在创建表的叉积,但丢弃了第二个表的项目。您的结果将只包含第一个重复表中的行。
  • 我不确定这样做比仅在实际表上使用 linq 有什么好处。您似乎使事情变得更复杂而收益却更少。我缺少创建此方法的原因吗?

标签: c# linq


【解决方案1】:

早期解决方案 ...

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;
}

您可以在选择查询中列出 table1 中的所有列。以下查询具有每个定义的 DataTable,其中包含 table1 中的所有列和 table2 中的键列。可能对你有帮助。

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

【讨论】:

  • Sria,我正在寻找的,正如我在原始帖子中所提到的,我需要从第一个数据表返回所有列,并且只返回第二个数据表的“连接列”。您的解决方案仅返回 2 列 - 每个表中 1 列。你能修改你的解决方案吗?
  • @HerrimanCoder 请检查更新版本。它可能对你有用。为了避免列出所有列,我在循环中添加了 table1 中的列。
  • 2 个问题: (1) foreach() 让它变得很慢——我之前做过类似的事情,想摆脱缓慢; (2) 连接正在执行某种交叉连接并产生超过 1M 的记录,而不是预期的 100K 记录。
  • 绝对不是交叉连接,但如果你处理 10 万条记录,我建议你使用数据库连接而不是 linq。
  • 抱歉耽搁了很久,我终于能够根据真实数据测试和验证这一点 - 效果很好!谢谢。
【解决方案2】:

有点模棱两可,但据我了解,在对Join 的结果应用Distinct() 后,您需要Join 两个表并从这两个表(或更少)中获取一行。所有这一切,假设列没有预定义。

这是我的解决方案:

  1. 添加一个Result 类来包装Join 的结果

    public class Result
    {
        public DataRow Table1Row { get; set; }
        public DataRow Table2Row { get; set; }
    
        public string DistictFieldValue { get; set; }
    }
    
  2. 添加一个ResultComparer 类来帮助您使用自己的逻辑来获得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. 更新您的方法以使用上述类

    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 行 + 一些任意值。我想返回连接中的所有行,以及第一个表中的所有列+第二个表中的连接列。你的解决方案能做到吗?我想尝试一下,但首先如果你能帮助我理解。谢谢。
  • 我对你的方法的一个困境是我最终为每个 Result 对象得到了 2 个独立且不相关的 DataRows。有没有一种简单的方法可以将它们组合成一个数据行和一个数据表?如果是这样,我可以删除我不想要的列并返回最终的数据表。
  • 它们并不是真正无关的,它们是 JOIN 的结果。要加入 2 行,您可以查看以下内容:stackoverflow.com/questions/16945569/…
  • 我需要避免使用foreaches,因为我每次要处理 1/2 百万行数据,这确实会减慢速度。当我离开 foreach 并转到 linq 加入时,我的性能显着提高 - 所以我不能回到循环。有没有办法拥有一组数据行并在new{} 中定义我想要的列?
【解决方案3】:

[更新 #3]

  1. 根据 INNER JOIN 行为,我不确定我返回的结果集是否正确。

linq 查询返回的结果集准确地代表了您在查询中编写的内容。

  1. 选择列表不包括第二个数据表的“连接列”(在 > 这个特定示例中,它应该是“ModelID”),并且我 需要它。

答案很简单:您的查询仅从第一个数据表返回数据(顺便说一句:您在问题描述中已经提到过)。

完成后,我可以确认 CMODEL 值 匹配 ModelID 值,从而确认我有一个有效的连接。 (这只是一种情况,它会有所不同,所以没有列名可以 在函数中进行硬编码。)

您可以确定 Linq2DataSet 查询返回正确的 ID。他们必须匹配才能加入他们。如果没有匹配,结果集将为空! 看来,您必须提高有关联接的知识。请阅读这篇精彩的文章:Visual Representation of SQL Joins

相关文章的简短版本:

左加入

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)

内连接

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

右加入

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)

交叉连接

cross join returns the cartesian product of the sets
  1. 如何区分结果集?

有一个Distinct method

但我不确定,你真的需要这个 ;(

一般说明:

有几种方法可以读取分隔文件 (*.csv):

1) 使用“标准”读取文本文件方法并在 [for] 循环中将文本拆分为多个部分

见:A Fast CSV Reader

2) 使用 linq 方法,即:Select()

注意:大多数程序员都知道,在处理大型数据集时,linq 方法会比 [for] 循环慢得多。
为了能够从连接表中投影字段,您必须使用:

select new {datarows1, datarows2}

如果您想使用 Linq 创建动态列,请参阅:Query datatable with dynamic column names using LINQ


这是一个完整的代码,如何将两个数据表连接成单个 数据表dotnetfiddle


3) 使用 OleDb:OleDbConnectionOleDbCommand

见:
Using OleDb To Import Text Files tab CSV Custom
Read Text File Specific Columns

您的扩展方法可能如下所示:

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;
}

致电:

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

要求:
- 两个 csv 文件必须在同一目录中
- csv 文件使用 csv 文件的标准分隔符,ee:Schema.ini file
- 文件中没有标题(没有列名)

【讨论】:

  • Maciej,感谢您非常彻底的回答。最重要的部分之一仍未得到解答:如何从第二个数据表中检索列。我相信一定有办法,因为它可以在代码时以声明方式完成;在运行时必须有某种方法可以做到这一点,直到运行时才知道模式。如果你能回答,我会奖励你的答案。例如,我希望能够将列名数组传递给函数,因为我希望将列值作为连接数据表的一部分。例如:{"SalesID", "FirstName", "SomeCode"} - 等等
  • 您的 [更新] 中的链接看起来很有希望,但它在我发布的示例中将如何工作?请使用动态列发布我的示例。谢谢。
  • @HerrimanCoder,我再次更新了我的答案。请找到指向 dotnetfiddle 的链接。在那里,您将找到如何将两个表连接到单个数据表中的完整代码。请检查OleDbJoin 方法。这很有趣。
【解决方案4】:

如果每个 CSV 文件代表数据库的一个表,那么考虑做一些类似于实体框架的事情。

让你的DbSets代替IQueryable&lt;...&gt;实现IEnumerable&lt;...&gt;

如果您只需要获取数据,这将相当容易。如果您还想更新,则需要实现(或重复使用)DbChangeTracker

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;
        }
    }
}

您还需要一个包含所有 DbSet 的 DbContext:

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 = ...};
}

您可以通过记住已获取的项目来提高性能。将它们放在字典中,使用主键作为字典键。还将Find 函数添加到 DbSet:

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;
    }
}

如果每个表项都具有相同类型的主键,则最简单:

interface IPrimaryKey
{
     int Id {get;}
}

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

如果没有,您需要告诉 DbSet 主键的类型:

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

如果您决定将您的项目保存在字典中,那么让您的 GetEnumerator 首先返回已获取的项目,然后再从您的 CSV 文件中获取新行。

添加/更新/删除项目

为此,您需要能够从您的 CsVFile 中添加/更新/删除项目。我假设已经有这样的功能。

要有效地进行更新,您需要类似于 DbContext.SaveChanges 的东西。使用 ChangeTracker 让每个 DbSet 记住要添加/删除/更新的项目:

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
    }

您需要一个记住原始值的 Find:

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;
}

最后是你的 SaveChanges()

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

显然,如果您希望能够更新数据库中的项目,您需要能够检查项目是否已更改。你需要一个 IEqualityComparer&lt;T&gt; 来检查值

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

DbSet 保存更改:

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

DbContext SaveChanges:

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

【讨论】:

  • 将 CSV 数据读入数据表已经非常快速和简单了;不是我要解决的问题。但无论如何感谢您周到的回答。我只是在寻找一种优雅的方式来返回仅在运行时已知的列,该列是由 2 个数据表的 LINQ 连接产生的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-28
  • 1970-01-01
  • 2020-01-09
  • 1970-01-01
  • 2016-12-19
  • 2013-02-16
相关资源
最近更新 更多