JSON.net 直接从 oledbconnection 序列化

Posted

技术标签:

【中文标题】JSON.net 直接从 oledbconnection 序列化【英文标题】:JSON.net serialize directly from oledbconnection 【发布时间】:2015-11-20 20:42:25 【问题描述】:

我目前有一个处理程序,它获取 excel 文件的文件路径和选项卡名,将文件处理为数据表,然后将表序列化为 json 字符串以返回。 这一直有效,直到我尝试处理大文件,然后出现内存不足异常。

我在想,如果我不先将所有内容加载到数据表中,而是直接加载到 json 字符串中,它会减少内存使用量。但是,我一直找不到任何有关如何执行此操作的示例。

我可以直接从 OleDbConnection 序列化为字符串吗?怎么样?

    public void ProcessRequest(HttpContext context)
    
        string path = context.Request["path"];
        string tableNames = context.Request["tableNames"];

        string connectionString = string.Empty;
        if (path.EndsWith(".xls"))
        
            connectionString = String.Format(@"Provider=Microsoft.ACE.OLEDB.12.0;
                Data Source=0;
                Extended Properties=""Excel 8.0;HDR=YES;IMEX=1""", path);
        
        else if (path.EndsWith(".xlsx"))
        
            connectionString = String.Format(@"Provider=Microsoft.ACE.OLEDB.12.0;
                Data Source=0;
                Extended Properties=""Excel 12.0 Xml;HDR=YES;IMEX=1""", path);
        
        DbProviderFactory factory = DbProviderFactories.GetFactory("System.Data.OleDb");

        DbDataAdapter adapter = factory.CreateDataAdapter();
        OleDbConnection conn = new OleDbConnection(connectionString);
        conn.Open();

        DataTable tmp = new DataTable();

        DbCommand selectCommand = factory.CreateCommand();

        selectCommand.CommandText = String.Format("SELECT * FROM [0]", tableNames);
        selectCommand.Connection = conn;
        adapter.SelectCommand = selectCommand;


        adapter.Fill(tmp);
        string tabdata = JsonConvert.SerializeObject(tmp);
        context.Response.Write(tabdata);
    

【问题讨论】:

内存在哪里耗尽?填写DataTable?或者创建string?最有可能首先创建字符串时内存不足,因为大字符串需要一个大的连续 2 字节字符数组。 是的,尝试填充字符串时内存不足。 【参考方案1】:

首先,您应该停止序列化到中间string,而是直接序列化到HttpResponse.OutputStream,使用以下简单方法:

public static class JsonExtensions

    public static void SerializeToStream(object value, System.Web.HttpResponse response, JsonSerializerSettings settings = null)
    
        if (response == null)
            throw new ArgumentNullException("response");
        SerializeToStream(value, response.OutputStream, settings);
    

    public static void SerializeToStream(object value, TextWriter writer, JsonSerializerSettings settings = null)
    
        if (writer == null)
            throw new ArgumentNullException("writer");
        var serializer = JsonSerializer.CreateDefault(settings);
        serializer.Serialize(writer, value);
    

    public static void SerializeToStream(object value, Stream stream, JsonSerializerSettings settings = null)
    
        if (stream == null)
            throw new ArgumentNullException("stream");
        using (var writer = new StreamWriter(stream))
        
            SerializeToStream(value, writer, settings);
        
    

由于一个大字符串需要一个大的连续内存块用于底层char 数组,这就是你将首先耗尽内存的地方。另请参阅 Json.NET 的 Performance Tips

为了最小化内存使用和分配的对象数量,Json.NET 支持直接对流进行序列化和反序列化。一次读取或写入 JSON,而不是将整个 JSON 字符串加载到内存中,这在处理大小超过 85kb 的 JSON 文档以避免 JSON 字符串最终进入大对象堆时尤为重要。

接下来,请务必将所有一次性用品包装在 using 语句中,如下所示。

这可能会解决您的问题,但如果没有,您可以使用以下JsonConverterIDataReader 序列化为 JSON:

public class DataReaderConverter : JsonConverter

    public override bool CanConvert(Type objectType)
    
        return typeof(IDataReader).IsAssignableFrom(objectType);
    

    public override bool CanRead  get  return false;  

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    
        throw new NotImplementedException();
    

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    
        var reader = (IDataReader)value;
        writer.WriteStartArray();
        while (reader.Read())
        
            writer.WriteStartObject();
            for (int i = 0; i < reader.FieldCount; i++)
            
                writer.WritePropertyName(reader.GetName(i));
                if (reader.IsDBNull(i))
                    writer.WriteNull();
                else
                    serializer.Serialize(writer, reader[i]);
            
            writer.WriteEndObject();
        
        writer.WriteEndArray();
    

然后序列化成流如下:

public static class ExcelExtensions

    private static string GetExcelConnectionString(string path)
    
        string connectionString = string.Empty;
        if (path.EndsWith(".xls"))
        
            connectionString = String.Format(@"Provider=Microsoft.ACE.OLEDB.12.0;
            Data Source=0;
            Extended Properties=""Excel 8.0;HDR=YES;IMEX=1""", path);
        
        else if (path.EndsWith(".xlsx"))
        
            connectionString = String.Format(@"Provider=Microsoft.ACE.OLEDB.12.0;
            Data Source=0;
            Extended Properties=""Excel 12.0 Xml;HDR=YES;IMEX=1""", path);
        
        return connectionString;
    

    public static string SerializeJsonToString(string path, string workSheetName, JsonSerializerSettings settings = null)
    
        using (var writer = new StringWriter())
        
            SerializeJsonToStream(path, workSheetName, writer, settings);
            return writer.ToString();
        
    

    public static void SerializeJsonToStream(string path, string workSheetName, Stream stream, JsonSerializerSettings settings = null)
    
        using (var writer = new StreamWriter(stream))
            SerializeJsonToStream(path, workSheetName, writer, settings);
    

    public static void SerializeJsonToStream(string path, string workSheetName, TextWriter writer, JsonSerializerSettings settings = null)
    
        settings = settings ?? new JsonSerializerSettings();
        var converter = new DataReaderConverter();
        settings.Converters.Add(converter);
        try
        
            string connectionString = GetExcelConnectionString(path);
            DbProviderFactory factory = DbProviderFactories.GetFactory("System.Data.OleDb");

            using (OleDbConnection conn = new OleDbConnection(connectionString))
            
                conn.Open();
                using (DbCommand selectCommand = factory.CreateCommand())
                
                    selectCommand.CommandText = String.Format("SELECT * FROM [0]", workSheetName);
                    selectCommand.Connection = conn;

                    using (var reader = selectCommand.ExecuteReader())
                    
                        JsonExtensions.SerializeToStream(reader, writer, settings);
                    
                
            
        
        finally
        
            settings.Converters.Remove(converter);
        
    

注意 - 经过轻微测试。在将其投入生产之前,请务必针对您现有的方法进行单元测试!对于转换器代码,我使用JSON Serialization of a DataReader 作为灵感。

更新

我的转换器以与 Json.NET 的 DataTableConverter 相同的结构发出 JSON。因此,您将能够使用 Json.NET 自动反序列化为 DataTable。如果您喜欢更紧凑的格式,您可以定义自己的格式,例如:


  "columns": [
    "Name 1",
    "Name 2"
  ],
  "rows": [
    [
      "value 11",
      "value 12"
    ],
    [
      "value 21",
      "value 22"
    ]
  ]

他们创建了以下转换器:

public class DataReaderArrayConverter : JsonConverter

    public override bool CanConvert(Type objectType)
    
        return typeof(IDataReader).IsAssignableFrom(objectType);
    

    public override bool CanRead  get  return false;  

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    
        throw new NotImplementedException();
    

    static string[] GetFieldNames(IDataReader reader)
    
        var fieldNames = new string[reader.FieldCount];
        for (int i = 0; i < reader.FieldCount; i++)
            fieldNames[i] = reader.GetName(i);
        return fieldNames;
    

    static void ValidateFieldNames(IDataReader reader, string[] fieldNames)
    
        if (reader.FieldCount != fieldNames.Length)
            throw new InvalidOperationException("Unequal record lengths");
        for (int i = 0; i < reader.FieldCount; i++)
            if (fieldNames[i] != reader.GetName(i))
                throw new InvalidOperationException(string.Format("Field names at index 0 differ: \"1\" vs \"2\"", i, fieldNames[i], reader.GetName(i)));
    

    const string columnsName = "columns";
    const string rowsName = "rows";

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    
        var reader = (IDataReader)value;
        writer.WriteStartObject();
        string[] fieldNames = null;
        while (reader.Read())
        
            if (fieldNames == null)
            
                writer.WritePropertyName(columnsName);
                fieldNames = GetFieldNames(reader);
                serializer.Serialize(writer, fieldNames);
                writer.WritePropertyName(rowsName);
                writer.WriteStartArray();
            
            else
            
                ValidateFieldNames(reader, fieldNames);
            

            writer.WriteStartArray();
            for (int i = 0; i < reader.FieldCount; i++)
            
                if (reader.IsDBNull(i))
                    writer.WriteNull();
                else
                    serializer.Serialize(writer, reader[i]);
            
            writer.WriteEndArray();
        
        if (fieldNames != null)
        
            writer.WriteEndArray();
        
        writer.WriteEndObject();
    

当然,您需要在客户端创建自己的反序列化转换器。

或者,您可以考虑压缩您的回复。我从未尝试过,但请参阅 HttpWebRequest and GZip Http Responses 和 ASP.NET GZip Encoding Caveats。

【讨论】:

谢谢,我想这对我有用! 实际上是 WriteJson 方法在构建字符串,对吗?我返回的结构具有每个数据行的列标题:["ColA":"20012511","ColB":"ABC","ColC":"EA","ColD":null, "ColA":"20013092","ColB":"DEF","ColC":"EA","ColD":"1", "ColA":"20013092","ColB":"GHI","ColC":"EA","ColD":"d"] 是否可以修改该方法以首先返回标题一次(可能是这样的):["ColA","ColB","ColC","ColD"], ["20012511","ABC","EA",:null, "20013092","DEF","EA","1", "20013092","GHI","EA","d"] @wham12 - 1) 根据jsonlint.com,您的备用 JSON 无效。我需要查看一个有效的示例才能发表评论。 2) 我的转换器旨在发出与 JSON.NET 的built-in data table converter 相同的 JSON。您是否发现我的转换器的行为与默认行为之间存在不一致? 是的,从我阅读的内容来看,听起来我只是在要求 JSON 不打算让你做的事情。我在想我们可以从每条记录中删除名称并将返回的数据大小减少一半(因为我知道我的列名将是什么)。 这正是我想要的!这被返回到一个 Handsontable,我很容易能够指向行和列数组。我使用您的新 WriteJson 和 GZip 内置的 IIS7 运行了一些测试。原始响应为 54.9MB,耗时 50.36 秒 - GZip 为 1.7MB,耗时 49.44 秒。列名只有一次的新格式为 19.4MB,耗时 45.76 秒 - GZip 为 1.1MB,耗时 51.49 秒。

以上是关于JSON.net 直接从 oledbconnection 序列化的主要内容,如果未能解决你的问题,请参考以下文章

Json.NET - 直接从流反序列化为动态?

使用 OleDbConnection 从数据库中提取数据

.xlsx 的 OleDbConnection.GetOleDbSchemaTable 无法识别隐藏的工作表

如何使用 C# 和 OleDbConnection 读取 .xlsx 和 .xls 文件?

具有区域设置特定字符的数据库的 OleDbConnection/命令

使用两个DataAdapter使用OleDbConnection Excel到DataGridView