Как добавить записи CsvHelper в DataTable для использования для SqlBulkCopy в базе данных

Я пытаюсь прочитать файл CSV с помощью CsvHelper, загрузить каждую запись в DataTable, а затем использовать SqlBulkCopy для вставки данных в таблицу базы данных. С текущим кодом я получаю исключение при добавлении строки в DataTable. Исключение: «Невозможно преобразовать объект типа« MvcStockAnalysis.Models.StockPrice »к типу« System.IConvertible ». Не удалось сохранить в столбце даты. Ожидаемый тип - DateTime».

Пример файла CSV от Yahoo Finance. Например: http://ichart.yahoo.com/table.csv?s=MMM&a=0&b=1&c=2010&d=0&e=17&f=2014&g=d&ignore=.csv

Файл CSV содержит следующий заголовок: Date Open High Low Close Volume Adj Close

Модель, в которую я читаю файл CSV:

namespace MvcStockAnalysis.Models
{
    using System;
    using System.Collections.Generic;

    public partial class StockPrice
    {
        public int Id { get; set; }
        public System.DateTime Date { get; set; }
        public int CompanyId { get; set; }
        public double High { get; set; }
        public double Low { get; set; }
        public double Close { get; set; }
        public double AdjClose { get; set; }
        public double Open { get; set; }
        public double Volume { get; set; }

        public virtual Company Company { get; set; }
    }
}

При сопоставлении файла CSV с классом StockPrice используется следующее:

public class StockPriceClassMap : CsvClassMap<StockPrice>
{
    public override void CreateMap()
    {
        Map(m => m.Date).Name("Date");
        Map(m => m.Close).Name("Close");
        Map(m => m.AdjClose).Name("Adj Close");
        Map(m => m.High).Name("High");
        Map(m => m.Low).Name("Low");
        Map(m => m.Open).Name("Open");
        Map(m => m.Volume).Name("Volume");
    }
}

Код, который пытается добавить записи CsvHelper в DataTable, выглядит следующим образом:

var connectionstring = ConfigurationManager.ConnectionStrings["MvcStockAnalysis.Models.MvcStockAnalysisContext"];
var connection = new SqlConnection();
connection.ConnectionString = connectionstring.ToString();
var destinationTableName = "StockPrices";
var company = db.Company
            .Where(c => c.Symbol == "MMM")
            .FirstOrDefault();

try
{
    string path = HttpContext.Server.MapPath("~/App_Data/" + company.Symbol + @".csv");

    if (System.IO.File.Exists(path))
    {     

        using (StreamReader sr = new StreamReader(path))
        {
            using (var csv = new CsvReader(sr))
            {
                DataTable dt = new DataTable("StockPrices");
                csv.Configuration.HasHeaderRecord = true;
                csv.Configuration.RegisterClassMap<StockPriceClassMap>();

                dt.Columns.Add(new DataColumn("Date", typeof(DateTime)));
                dt.Columns.Add(new DataColumn("Close", typeof(Double)));
                dt.Columns.Add(new DataColumn("AdjClose", typeof(Double)));
                dt.Columns.Add(new DataColumn("High", typeof(Double)));
                dt.Columns.Add(new DataColumn("Low", typeof(Double)));
                dt.Columns.Add(new DataColumn("Open", typeof(Double)));
                dt.Columns.Add(new DataColumn("Volume", typeof(Double)));
                dt.Columns.Add(new DataColumn("CompanyId", typeof(Double)));
                var records = csv.GetRecords<StockPrice>().ToList();
                foreach (var record in records)
                {                                    
                    record.CompanyId = company.Id;
                    dt.Rows.Add(record);
                }
                // add dt to the database
                using (var bulkCopy = new SqlBulkCopy(connection.ConnectionString))
                {
                    // DataTable column names match my SQL Column names, so I simply made this loop. 
                    foreach (DataColumn col in dt.Columns)
                    {
                        bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);
                    }
                    bulkCopy.DestinationTableName = destinationTableName;
                    bulkCopy.WriteToServer(dt);
                }
            }
        }
    }
    connection.Close();
}
catch (Exception e)
{
    Console.Write(e.Message);
}

Как я могу добавить записи CsvHelper в DataTable, чтобы использовать их для SqlBulkCopy в базе данных?


person Justin Nafe    schedule 21.01.2014    source источник


Ответы (4)


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

while( csv.Read() )
{
    var row = dt.NewRow();
    foreach( DataColumn column in dt.Columns )
    {
        row[column.ColumnName] = csv.GetField( column.DataType, column.ColumnName );
    }
    dt.Rows.Add( row );
}
person Josh Close    schedule 22.03.2015
comment
Голосование за то, что ... вы знаете ... вы являетесь автором библиотеки - person Joe Phillips; 30.11.2015
comment
Если бы был способ получить IndexOf(colName) из карты, это помогло бы, когда порядок столбцов DataTable не соответствует порядку CSV. - person Ňɏssa Pøngjǣrdenlarp; 15.03.2016
comment
@JoePhillips И что? Не может автор библиотеки ответить на вопрос? - person Stefan Fachmann; 21.06.2018
comment
@StefanFachmann Да, поэтому я проголосовал за. Перечитайте мой комментарий - person Joe Phillips; 21.06.2018

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

    private DataTable BuildDataTable()
    {
        var dt = new DataTable();

        using (var textReader = new StreamReader(_path))
        {
            using (var csv = new CsvReader(textReader))
            {
                csv.ReadHeader();
                foreach (var header in csv.FieldHeaders)
                {
                    dt.Columns.Add(header);
                }


                while (csv.Read())
                {
                    var row = dt.NewRow();
                    foreach (DataColumn column in dt.Columns)
                    {
                        row[column.ColumnName] = csv.GetField(column.DataType, column.ColumnName);
                    }
                    dt.Rows.Add(row);
                }
            }
        }

        return dt;
    }
person Rahul Misra    schedule 09.10.2017

Мне удалось заставить это работать, добавив строку DataTable и явно заполнив ее, вместо того, чтобы пытаться добавить запись CsvHelper в виде строки.

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

foreach (var record in records)
{
    DataRow row = dt.NewRow();
    record.CompanyId = company.Id;
    row["Date"] = record.Date;
    row["Close"] = record.Close;
    row["AdjClose"] = record.AdjClose;
    row["High"] = record.High;
    row["Low"] = record.Low;
    row["Open"] = record.Open;
    row["Volume"] = record.Volume;
    row["CompanyId"] = record.CompanyId;
    dt.Rows.Add(row);
}

Если вы сможете решить проблему, не прибегая к жесткому программированию, я приму ваш ответ как ответ.

person Justin Nafe    schedule 23.01.2014

Мне понравился ответ @JoshClose, но я обнаружил, что while( csv.Read() ) значительно медленнее, чем csv.GetRecords<{Class}>().ToList(). Он также неправильно обрабатывает многие типы, допускающие значение NULL, такие как int?, когда возвращаемое значение должно быть DBNull. Мой ответ - заставить CsvHelper импортировать список динамических записей, а затем использовать пару вспомогательных методов для автоматического сопоставления с DataTable.

var records = csv.GetRecords<dynamic>().ToList();

foreach ( record in records )
{
    var row = dt.NewRow();

    var recordDictionary = DynamicToDictionary( record );

    foreach( DataColumn column in dt.Columns )
    {
        row[column.ColumnName] = GetColumnValue( column, recordDictionary );
    }

    dt.Rows.Add( row );
}

Метод DynamicToDictionary обрабатывает чувствительность к регистру и пробелы в заголовке. Я преобразовываю динамический объект в объект Dictionary, который игнорирует чувствительность к регистру и удаляет пробелы в заголовке. Это можно пропустить, а динамический объект передать непосредственно GetColumnValue, если это не проблема.

public Dictionary<string, object> DynamicToDictionary(dynamic dynObj)
{
    var dictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

    foreach (var kvp in (IDictionary<string, object>) dynObj)
    {
        var obj = kvp.Value;

        // Remove white space.
        var name = new string(kvp.Key.ToCharArray().Where(c => !char.IsWhiteSpace(c)).ToArray());

        dictionary.Add(name, obj);
    }

    return dictionary;
}

Метод GetColumnValue находит и преобразует значение динамической записи в правильное значение столбца DataTable.

public object GetColumnValue(DataColumn column, IDictionary<string, object> dynamicDictionary)
{
    object value;

    // Return DBNull if the column name isn't found.
    if (!dynamicDictionary.TryGetValue(column.ColumnName, out value))
    {
        return DBNull.Value;
    }

    // Null values come in as empty strings.
    if (column.AllowDBNull && column.DataType != typeof(string) && (string)value == "")
    {
        return DBNull.Value;
    }

    if (column.DataType == typeof(bool))
    {
        return (string)value != "0" && ((string)value).ToLower() != "false";
    }

    return value;
}
person David Specht    schedule 12.09.2017