Как сделать соединение LINQ, которое ведет себя точно так же, как физическое внутреннее соединение базы данных? - PullRequest
0 голосов
/ 08 января 2019

РЕДАКТИРОВАТЬ : Мой первоначальный вопрос был запутанным и неоднозначным, поэтому позвольте мне начать снова.

Источник данных представляет собой набор файлов CSV, поэтому фактической базы данных нет. Это интеграция с унаследованной десятилетней системой в Японии.

У меня есть функция c #, которая должна принимать 2 DataTables и имена двух столбцов в качестве параметров. Моя функция должна сделать эквивалент INNER JOIN для этих 2 таблиц данных, а затем вернуть все столбцы из первой таблицы и только «объединенный столбец» из 2-й таблицы. Схемы (читай: столбцы) этих таблиц данных не будут известны до времени выполнения, поэтому у функции не может быть жестко заданных имен столбцов. Наконец, моя функция должна вернуть новый объект DataTable с внутренними данными и набор результатов DISTINCT, основанный на только что указанном списке выбора.

Вот моя [измененная] попытка, которая, похоже, дает многообещающий набор результатов:

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. Список выбора не включает в себя «столбец соединения» для 2-й таблицы данных (в данном конкретном примере это будет «ModelID»), и мне это нужно. После этого я могу подтвердить, что значения CMODEL соответствуют значениям ModelID, и, таким образом, подтвердить, что у меня есть допустимое соединение. (Это всего лишь 1 сценарий, он будет другим, поэтому имена столбцов не могут быть жестко закодированы в функции.)
  3. Как отличить результирующий набор?

Вот один конкретный пример из моей системы, но опять же, таблицы данных и схемы будут разными:

dtCSV столбцы:

  1. CModel
  2. CATT_CD
  3. NSTAND
  4. CAPPLY1
  5. DREFIX_D

_dtModelOptions столбцы:

  1. SeriesID
  2. ModelID
  3. OptionID

Какие изменения мне нужны для моей функции, чтобы:

  1. Это ВНУТРЕННЕЕ СОЕДИНЕНИЕ и ОТЛИЧИЕ (он уже делает это?)
  2. Он выбирает все столбцы из первой таблицы и только "столбец соединения" из второй таблицы (в настоящее время он получает только столбцы первой таблицы)
  3. Производительность настолько высока, насколько это возможно (ранее я foreach просматривал записи для достижения объединения, и этот подход был ужасно медленным.)

Спасибо за любой совет, я действительно ценю ценное время каждого.

Ответы [ 4 ]

0 голосов
/ 23 января 2019

Если каждый 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;
        }
    }
}

Вам также понадобится DbContext, который содержит все ваши DbSets:

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 сначала возвращает уже fetchedItems, прежде чем извлекать новые строки из вашего CSV-файла.

Добавить / обновить / удалить элементы

Для этого вам нужно иметь возможность добавлять / обновлять / удалять элементы из вашего 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
    }

Вам понадобится 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<T>, который проверяет по значению

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

DbSet SaveChanges:

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

DbContext SaveChanges:

Students.SaveChanges()
Teachers.SaveChanges();
Schools.SaveChanges();
0 голосов
/ 18 января 2019

[Обновление № 3]

  1. Я не уверен, верен ли набор результатов, который я получаю, исходя из поведения INNER JOIN.

Набор результатов, возвращаемый запросом linq, представляет собой именно то, что вы написали в запросе.

  1. Список выбора не включает «столбец соединения» для 2-й таблицы данных (в этом конкретном примере это будет «ModelID»), и я нужно это

Ответ очень прост: ваш запрос возвращает данные только из первых данных (кстати, вы уже упоминали об этом в описании проблемы).

Как только он это сделает, я могу подтвердить, что значения CMODEL сопоставьте значения ModelID и, таким образом, подтвердите, что у меня есть допустимое соединение. (Это всего лишь 1 сценарий, он будет другим, поэтому имена столбцов не могут быть жестко закодированным в функции.)

Вы можете быть уверены, что запрос Linq2DataSet возвращает правильные идентификаторы. Они должны соответствовать, чтобы иметь возможность присоединиться к ним. Если совпадений нет, набор результатов будет пустым! Кажется, ты должен улучшить свои знания о соединениях. Пожалуйста, прочитайте эту прекрасную статью: Визуальное представление соединений SQL

Короткая версия связанной статьи:

левое соединение

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. Как отличить набор результатов?

Существует Отличный метод .

Но я не уверен, вам это действительно нужно; (

Общее примечание:

Существует несколько способов чтения файлов с разделителями (* .csv):

1) с использованием "стандартных" методов чтения текстового файла и разбиения текста на части в цикле [for]

См .: Быстрый считыватель CSV

2) с использованием методов linq, т.е.: Select()

Примечание: Большинство программистов хорошо знают, что методы linq будут работать намного медленнее, чем цикл [for], при работе с большими наборами данных.
Чтобы иметь возможность проецировать поля из объединенных таблиц, вы должны использовать:

select new {datarows1, datarows2}

Если вы хотите создать динамические столбцы с помощью Linq, см .: Запрос данных с динамическими именами столбцов с использованием LINQ


Вот полный код, как объединить две таблицы данных в один datatable : dotnetfiddle


3) с использованием OleDb: OleDbConnection , OleDbCommand

См:
Использование OleDb для импорта текстовых файлов на вкладке CSV Custom
Чтение определенных текстовых файлов столбцов

Ваш метод расширения может выглядеть следующим образом:

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-файлов, например: Файл Schema.ini
- в файлах нет заголовков (без имен столбцов)

0 голосов
/ 22 января 2019

Существует некоторая двусмысленность, но, насколько я понимаю, вам нужно 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;
    }
    
0 голосов
/ 09 января 2019

Ранее Soution ...

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

Вы можете перечислить все столбцы из таблицы1 в запросе выбора. Следующий запрос имеет определенный DataTable со всеми столбцами из таблицы1 и только ключевой столбец из таблицы2. Это может помочь вам.

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();
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...