如何执行与物理数据库内部联接完全一样的LINQ联接?

时间:2019-01-08 00:51:55

标签: c# linq

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

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

我有一个c#函数,需要使用2个DataTables和2个列名作为参数。我的函数需要在这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. NSTAND
  4. CAPPLY1
  5. CAPPLY2
  6. DREFIX_D

_dtModelOptions 列:

  1. SeriesID
  2. ModelID
  3. OptionID

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

  1. 它会进行INNER JOIN和DISTINCT(是否已经这样做?)
  2. 它从第一个表中选择所有列,并从第二个表中选择“ join-column”(当前仅获取第一个表的列)
  3. 性能要尽可能快(我以前foreach遍历记录以实现联接,而这种方法非常慢。)

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

4 个答案:

答案 0 :(得分: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;
}

您可以在select查询中列出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();
}

答案 1 :(得分:0)

[更新#3]

  
      
  1. 我不确定基于INNER JOIN行为返回的结果集是否正确。
  2.   

linq查询返回的结果集完全代表您在查询中编写的内容。

  
      
  1. 选择列表不包含第二个数据表的“联接列”(在此特定示例中为“ ModelID”),并且我   需要它。
  2.   

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

  

一旦这样做,我可以确认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. 如何区分结果集?
  2.   

有一个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
-文件中没有标题(没有列名)

答案 2 :(得分:0)

有点歧义,但是据我了解,您需要Sub TestMe() If Weekday(Now()) = vbMonday Then SomeSelection SomeSelection SomeSelection End If End Sub Sub SomeSelection() 'OP Code End Sub 两个表并在对结果执行Sub TestMe() Dim repeater As Long: repeater = 1 If Weekday(Now()) = vbMonday Then repeater = 3 Dim counter As Long For counter = 1 To repeater Range("B8").End(xlToRight).Offset(, 0).Select 'OP code... ActiveSheet.Paste Next End Sub 之后从两个表(或更少)中获取一行。 Join。考虑到这些列不是预定义的,所有这些。

这是我的解决方法:

  1. 添加一个Distinct()类以包装Join的结果

    Result
  2. 添加一个Join类以帮助您使用自己的逻辑来获取public class Result { public DataRow Table1Row { get; set; } public DataRow Table2Row { get; set; } public string DistictFieldValue { get; set; } } 结果

    ResultComparer
  3. 更新您的方法以使用上述类

    Distinct()

答案 3 :(得分:0)

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

让您的IQueryable<...>实现DbSets而不是IEnumerable<...>

如果只需要获取数据,这将非常容易。如果您还想更新,则需要实现(或重复使用)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 = ...};
}

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

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

如果您决定将项目保留在Dictionary中,那么在从CSV文件中获取新行之前,让GetEnumerator首先返回已经获取的items。

添加/更新/删除项目

为此,您需要能够从CsVFile添加/更新/删除项目。我认为已经有相应的功能。

要有效地执行更新,您将需要类似于DbContext.SaveChanges的内容。让每个DbSet都使用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
    }

您将需要一个可以记住原始值的查找:

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<T>来按值检查

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

DbSet保存更改:

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

DbContext保存更改:

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