Рекомендации по способам объединения записей CSV в таблицу SQL в ASP .NET - PullRequest
1 голос
/ 15 февраля 2011

У меня есть приложение ASP .NET MVC, для которого я пытаюсь написать функцию импорта.

У меня есть некоторые особенности, например, я использую Entity Framework v4 в приложении MVC , но я особенно заинтересован в алгоритме , который будет работать лучше, предпочтительно с объяснение того, что это за производительность и почему.

Эта операция будет выполняться асинхронно, поэтому время выполнения не так важно, как использование ОЗУ.

Я должен отметить, что есть несколько вещей (база данных является основной), которые я был вынужден унаследовать, и из-за нехватки времени я не смогу очистить их до более поздней даты.

Детали

Функция импорта заключается в том, чтобы взять CSV-файл в памяти (который был экспортирован из Sales Force и загружен) и объединить его с существующей таблицей базы данных. Процесс должен быть подготовлен к:

  • Обновление существующих записей, которые могли быть изменены в CSV, без удаления повторного добавления записи базы данных, чтобы сохранить первичный ключ каждой записи.

  • Добавление и удаление любых записей по мере их изменения в файле CSV.

Текущая структура таблицы CSV и базы данных такова:

  • Таблица и CSV содержат 52 столбца.

  • Каждый столбец в существующей схеме базы данных является полем VARCHAR (100) ; Я планирую оптимизировать это, но не могу в течение текущего периода времени.

  • Сервер базы данных - MS SQL.

  • В CSV-файле содержится около 1700 строк данных. Я не вижу этого числа, превышающего 5000, так как, похоже, уже много повторяющихся записей.

  • В настоящее время я планирую только импортировать 10 из этих столбцов из CSV, остальные поля таблицы останутся пустыми, а позже я буду удалять ненужные столбцы.

  • CSV-файл считывается в DataTable , чтобы с ним было легче работать.

  • Сначала я думал, что поле ContactID в моем CSV Sales Force было уникальным идентификатором, хотя после выполнения некоторых тестовых импортов кажется, что в самом файле CSV есть ноль уникальных полей, по крайней мере, что я могу найти.

  • Учитывая это, я был вынужден добавить поле первичного ключа в таблицу контактов, чтобы другие таблицы могли поддерживать действительные отношения с любым данным контактом. Однако это, очевидно, не позволяет мне просто удалять и заново создавать записи при каждом импорте.

BEGIN EDIT

Мне ясно, что то, чего я пытался достичь, выполнить обновления существующих записей базы данных, когда не существует никакой связи между таблицей и CSV, просто не может быть достигнуто.

Это было не так много, что я не знал об этом заранее, но больше, что я надеялся, была просто какая-то блестящая идея, о которой я не думал, которая могла бы сделать это.

Имея это в виду, я решил в своем алгоритме просто предположить, что ContactID является уникальным идентификатором, а затем посмотреть, сколько дубликатов у меня получилось.

Я собираюсь найти возможное решение в качестве ответа ниже. И алгоритм, и фактическая реализация. Я оставлю это на несколько дней, потому что я бы предпочел принять в качестве ответа чье-то лучшее решение.

Вот некоторые вещи, которые я нашел после реализации моего решения ниже:

  • Мне пришлось сузить строки, предоставляемые CSV, чтобы они соответствовали тем строкам, которые импортируются в базу данных.
  • SqlDataReader прекрасно работает, и наибольшее влияние оказывают отдельные запросы UPDATE / INSERT, которые выполняются.
  • Для полностью нового импорта начальное чтение элементов в память не замечается пользовательским интерфейсом, процесс вставки занимает около 30 секунд.
  • Было только 15 дублированных идентификаторов, пропущенных при новом импорте, что составляет менее 1% от общего набора данных. Я посчитал это приемлемой потерей, поскольку мне сказали, что база данных отдела продаж в любом случае подвергнется очистке. Я надеюсь, что идентификаторы могут быть восстановлены в этих случаях.
  • Я не собирал никаких метрик ресурса во время импорта, но с точки зрения скорости это нормально, потому что индикатор прогресса, который я реализовал, чтобы предоставить обратную связь пользователю.

END EDIT

Ресурсы

Учитывая размер выделения каждого поля, даже с этим относительно небольшим количеством записей, меня больше всего беспокоит объем памяти, который может быть выделен во время импорта.

Приложение не будет работать в общей среде, поэтому в этом отношении есть место для вдохновения. Кроме того, эта конкретная функция будет запускаться только один раз в неделю или около того, вручную.

Моя цель, по крайней мере, быть в состоянии комфортно работать на полуотделенной машине. Спецификации машины являются переменными, поскольку приложение может в конечном итоге продаваться как продукт (хотя, опять же, не предназначенный для общей среды).

С точки зрения времени выполнения процесса импорта, как уже упоминалось, это будет асинхронно, и я уже собрал несколько вызовов AJAX и индикатор выполнения. Так что я думаю, что где-нибудь до минуты или двух будет в порядке.

Решение

Я нашел следующий пост, который, кажется, близок к тому, что я хочу:

Сравните две таблицы данных, чтобы определить строки в одной, но не в другой

Мне кажется, что выполнение поиска по хеш-таблице - правильная идея. Однако, как уже упоминалось, если мне удастся избежать загрузки как CSV, так и таблицы контактов в память полностью, это было бы предпочтительным, и я не вижу возможности избежать этого с помощью метода хеш-таблицы.

Одна вещь, которую я не уверен в том, как добиться этого, - как вычислить хеш каждой строки для сравнения, когда один набор данных является объектом DataTable, а другой - элементами EntitySet of Contact.

Я думаю, что если я не хочу вручную выполнять итерацию по каждому значению столбца для вычисления хэша, мне потребуется, чтобы оба набора данных были одного и того же типа объекта, если у кого-то нет каких-либо причудливых решений.

Лучше ли мне просто забыть Entity Framework для этой процедуры? Я, конечно, потратил много времени, пытаясь дистанционно выполнить массовые операции, поэтому я более чем счастлив удалить его из уравнения.

Если что-то не имеет смысла или отсутствует, прошу прощения, я очень устал. Просто дайте мне знать, и я исправлю это завтра.

Я ценю любую помощь, которая может быть предложена, так как я начинаю отчаяться. Я потратил больше времени на то, чтобы приблизиться к этому, чем планировал.

Спасибо!

Ответы [ 3 ]

2 голосов
/ 18 февраля 2011

В зависимости от ваших временных шкал я бы (и использовал) просто использовал DBAmp от Forceamp .Он представлен как драйвер OLE DB и, следовательно, используется в качестве связанного сервера в SQL Server.

Стандартное использование инструмента - использование поставляемых хранимых процедур для репликации / обновления схемы Salesforce для SQL Server.Я делаю это в некоторых очень больших средах, и это достаточно эффективно, чтобы обновлять каждые 15 минут без наложения.

DBAmp поддерживает типы столбцов в базовых таблицах SQL Server.

Еще один момент, остерегайтесь 15charИдентификаторы Salesforce (идентификаторы SObject).Они уникальны только при сравнении с учетом регистра.В отчетах Salesforce обычно выводятся 15-значные идентификаторы, но дампы API обычно представляют собой 18-значные без учета регистра идентификаторы.Подробнее о конвертации и т. Д. здесь .Если вы все еще видите коллизии при сравнении с учетом регистра, я склонен думать, что для файла выполняется некоторая предварительная обработка или, возможно, ошибка в отчете, используемом для экспорта.

В дополнение к вашему комментарию,Идентификаторы Salesforce глобально уникальны, то есть они не повторяются между различными производственными организациями клиента.Таким образом, даже если вы извлекаете записи из нескольких организаций, они не должны сталкиваться.Органы песочницы полной копии имеют идентичные идентификаторы для идентификатора "главной" рабочей организации.

Если вы заинтересованы в использовании API, обратитесь непосредственно к библиотеке Salesforce.Net , что довольно неплохо.чтобы начать.

0 голосов
/ 18 февраля 2011

Я предложил следующее возможное решение.После его реализации, используя мой тестовый CSV, я обнаружил, что было всего 15 дубликатов ContactID.

Учитывая, что это менее 1% от текущего набора контактов, я счел это приемлемым.

Чтобы это работало, я должен был сделать столбцы, предоставляемые CSV, равными столбцам, импортированным приложением, иначе сравнение, очевидно, не получится.

Вот алгоритм, который я собрал:

        /* Algorithm:
         * ----------
         * I'm making the assumption that the ContactID field is going to be unique, and if not, I will ignore any duplicates.
         * The reason for this is that I don't see a way to be able to update an existing database record with the changes in the CSV
         * unless a relationship exists to indicate what CSV record relates to what database record.
         * 
         * - Load DB table into memory
         * - Load CSV records into memory
         * - For each record in the CSV:
         *      - Add this record's contact ID to a list of IDs which need to remain the DB. 
         *        If it already exists in the list, we have a duplicate ID. Skip.
         *        
         *      - Concatenate CSV column values into a single string, store for later comparison.
         *      
         *      - Select the top record from the DB DataTable where: the ContactID field in the DB record matches that in the CSV.
         *      
         *      - If no DB records were found
         *          - Add this new record to the DB.
         *          
         *      - Concatenate column values for the DB record and compare this to the string generated previously.
         *      - If the strings match, skip any further processing
         *       
         *      - For each column in the CSV record:
         *          - Compare against the value for the same column in the DB record.
         *          - If values do not match, use StringBuilder to add to your UPDATE query for this record.
         *          
         * 
         * - Now we need to clean out the records from the DB which no longer exist in the CSV. Use the previously built list of ContactIDs.
         * - For each record in the DB:
         *      - If the ContactID in the DB record is not in your list, use a StringBuilder to add this ID to a DELETE statement. eg. OR [ContactID] = ...
         *      
         */

И вот моя реализация:

public class ContactImportService : ServiceBase
{

    private DataTable csvData;

    //...   

    public void DifferentialImport(Guid ID)
    {

        //This is a list of ContactIDs which we come across in the CSV during processing.
        //Any records in the DB which have an ID not in this list will be deleted.
        List<string> currentIDs = new List<string>();

        lock (syncRoot)
        {
            jobQueue[ID].TotalItems = (short)csvData.Rows.Count;
            jobQueue[ID].Status = "Loading contact records";
        }

        //Load existing data into memory from Database.
        SqlConnection connection = 
            new SqlConnection(Utilities.ConnectionStrings["MyDataBase"].ConnectionString);
        SqlCommand command = new SqlCommand("SELECT " +
                "[ContactID],[FirstName],[LastName],[Title]" +
                // Etc...
                "FROM [Contact]" +
                "ORDER BY [ContactID]", connection);

        connection.Open();
        SqlDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection);
        DataTable dbData = new DataTable();
        dbData.Load(reader);
        reader = null;

        lock (syncRoot)
        {
            jobQueue[ID].Status = "Merging records";
        }

        int affected = -1;
        foreach (DataRow row in csvData.Rows)
        {
            string contactID = row["ContactID"].ToString();
            //Have we already processed a record with this ID? If so, skip.
            if (currentIDs.IndexOf(contactID) != -1)
                break;

            currentIDs.Add(row["ContactID"].ToString());

            string csvValues = Utilities.GetDataRowString(row);

            //Get a row from our DB DataTable with the same ID that we got previously:
            DataRow dbRecord = (from record in dbData.AsEnumerable()
                            where record.Field<string>("ContactID") == contactID
                            select record).SingleOrDefault();

            //Found an ID not in the database yet... add it.
            if (dbRecord == null)
            {
                command = new SqlCommand("INSERT INTO [Contact] " +
                    "... VALUES ...", connection);
                connection.Open();
                affected = command.ExecuteNonQuery();
                connection.Close();
                if (affected < 1)
                {
                    lock (syncRoot)
                    {
                        jobQueue[ID].FailedChanges++;
                    }
                }
            }

            //Compare the DB record with the CSV record:
            string dbValues = Utilities.GetDataRowString(dbRecord);

            //Values are different, we need to update the DB to match.
            if (csvValues == dbValues)
                continue;

            //TODO: Dynamically build the update query based on the specific columns which don't match using StringBulder.
            command = new SqlCommand("UPDATE [Contact] SET ... WHERE [Contact].[ContactID] = @ContactID");
            //...
            command.Parameters.Add("@ContactID", SqlDbType.VarChar, 100, contactID);
            connection.Open();
            affected = command.ExecuteNonQuery();
            connection.Close();

            //Update job counters.
            lock (syncRoot)
            {
                if (affected < 1)
                    jobQueue[ID].FailedChanges++;
                else
                    jobQueue[ID].UpdatedItems++;
                jobQueue[ID].ProcessedItems++;
                jobQueue[ID].Status = "Deleting old records";
            }

        } // CSV Rows

        //Now that we know all of the Contacts which exist in the CSV currently, use the list of IDs to build a DELETE query
        //which removes old entries from the database.
        StringBuilder deleteQuery = new StringBuilder("DELETE FROM [Contact] WHERE ");

        //Find all the ContactIDs which are listed in our DB DataTable, but not found in our list of current IDs.
        List<string> dbIDs = (from record in dbData.AsEnumerable()
                              where currentIDs.IndexOf(record.Field<string>("ContactID")) == -1
                              select record.Field<string>("ContactID")).ToList();

        if (dbIDs.Count != 0)
        {
            command = new SqlCommand();
            command.Connection = connection;
            for (int i = 0; i < dbIDs.Count; i++)
            {
                deleteQuery.Append(i != 0 ? " OR " : "");
                deleteQuery.Append("[Contact].[ContactID] = @" + i.ToString());
                command.Parameters.Add("@" + i.ToString(), SqlDbType.VarChar, 100, dbIDs[i]);
            }
            command.CommandText = deleteQuery.ToString();

            connection.Open();
            affected = command.ExecuteNonQuery();
            connection.Close();
        }

        lock (syncRoot)
        {
            jobQueue[ID].Status = "Finished";
        }

        remove(ID);

    }

}

SqlDataReader кажется достаточным, это отдельные запросы UPDATE, которые занимают большую часть времени, все другие операции ничтожны в сравнении.

Я бы сказал, что на этом этапе требуется около 30 секунд, чтобы выполнить новый импорт, где все записи должны быть импортированы.С помощью обратной связи о прогрессе, которую я реализовал, это достаточно быстро для конечного пользователя.

Я еще не измерял использование ресурсов.

0 голосов
/ 16 февраля 2011

Учитывая тот факт, что вы будете иметь дело не более чем с 5000 строками одновременно, я склонен использовать ADO.Net (вероятно, SQLDataReader) только для передачи данных в объекты. Первичные ключи WRT - я не знаю о деталях экспортированных Salesforce данных, но комментарий c.f @ superfell. Если нет, вы можете создать свои собственные PK для объектов).

Затем я мог бы использовать методы, доступные классу List<T>, для фильтрации / итерации по строкам путем сравнения последовательных полей и т. Д.

Это в основном обусловлено тем фактом, что мой C# во много раз лучше, чем мой SQL; -)

Удачи.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...