C# JSON反序列化带有外键约束的类型数据集

huangapple go评论69阅读模式
英文:

C# JSON deserialization of a typed dataset with foreign key constraints

问题

我尝试使用C#和NewtonSoft.Json包将类型化的DataSets序列化和反序列化为JSON。这在没有外键约束的DataSets上可以正常工作,但对于包含外键约束并且父表在子表之后添加到设计中的DataSets则无法正常工作。在自动生成的Designer.cs中,父表在子表之后添加到基本的DataTableCollection中。

当DataSets被序列化为JSON时,子表首先被序列化。然后,当将JSON反序列化回类型化的DataSet实例时,会抛出InvalidConstraintException异常("ForeignKeyConstraint需要子表键值存在于父表中")。

为了创建一个测试场景,我创建了一个非常简单的类型化DataSet,名为ChildParentDataSet。它包含两个表。第一个表名为Child,其中包含一个名为ParentId的列。第二个表名为Parent,其中包含一个名为Id的列。在Parent.Id和Child.ParentId之间创建了一个外键约束。重要的是,Parent表在Child表之后添加到设计中。下面的代码会在最后一行引发InvalidConstraintException:

ChildParentDataSet ds = new ChildParentDataSet();
ds.Parent.Rows.Add(1);
ds.Child.Rows.Add(1);
string json = Newtonsoft.Json.JsonConvert.SerializeObject(ds);
ChildParentDataSet? ds2 = Newtonsoft.Json.JsonConvert.DeserializeObject<ChildParentDataSet>(json);

我首选的方法是在反序列化期间处理这个问题,通过实现自己的CustomCreationConverter来初始化DataSet.EnforceConstraints为false,然后进行反序列化,完成后将EnforceConstraints设置为true。以下是我希望实现此目标的类:

public class DataSetDeserializer
{
   public static T DeserializeDataSet<T>(string json)
   {
      var ds = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(json, new DataSetConverter<T>());
      System.Data.DataSet? dataSet = (System.Data.DataSet?)(Object?)ds;
      dataSet.EnforceConstraints = true;
      return ds;
   }

   private class DataSetConverter<T> : Newtonsoft.Json.Converters.CustomCreationConverter<T>
   {
      public override T Create(Type objectType)
      {
         var ds = Activator.CreateInstance(objectType);
         System.Data.DataSet? dataSet = (System.Data.DataSet?)ds;
         dataSet.EnforceConstraints = false;
         return (T)ds;
      }
   }
}

然后,我将测试代码的最后一行更改为使用DataSetDeserializer类进行反序列化,而不是使用Newtonsoft进行反序列化:

ChildParentDataSet? ds2 = DataSetDeserializer.DeserializeDataSet<ChildParentDataSet>(json);

当运行此代码时,将从调用DeserializeObject抛出Newtonsoft.Json.JsonSerializationException异常。异常消息是"无法将JSON对象填充到类型'ChildParentDataSet'。路径'Child',行1,位置9。"我还没有弄清楚这个问题。

另一个选择是在序列化而不是反序列化期间处理此问题。我实现了一个方法,按照表的层次将表排序,使最高级别的父表首先出现,最低级别的子表最后出现。然后,按正确的顺序逐个序列化每个表,并连接所有JSON字符串。这样可以解决问题,但关键是它无法处理表通过自引用外键成为自身的父表和子表的情况。我可能可以找出如何处理这种情况,但这将更加困难。而且,我的当前实现与一次性调用序列化整个DataSet相比效率相当低下。

一个可行的选项是使用XML而不是JSON。这肯定会更容易,因为DataSet有一个GetXml函数,但XML更加冗长,而且我通过gRPC通过网络传输这些数据。此外,它需要在许多客户端和服务的复杂重新部署。

我当然愿意听取建议。我相信有更好的替代方案我尚未考虑。

编辑:@Serge建议我发布ChildParentDataSet的代码。不幸的是,这是大量的自动生成代码,无法在此处发布,但他后来建议我在代码中创建等效的内容。我已经这样做了,下面是示例:

internal class ChildParentDataSet2 : DataSet
{
   public DataTable Child { get; set; }
   public DataTable Parent { get; set; }

   public ChildParentDataSet2()
   {
      this.Child = this.Tables.Add("Child");
      DataColumn childColumn = this.Child.Columns.Add("ParentId", typeof(int));
      this.Parent = this.Tables.Add("Parent");
      DataColumn parentColumn = this.Parent.Columns.Add("Id", typeof(int));
      this.Parent.PrimaryKey = new DataColumn[] { parentColumn };
      this.Child.ParentRelations.Add(parentColumn, childColumn);
   }
}

奇怪的是,当使用这个类而不是自动生成的类时,可以成功地反序列化,使用我的DataSetDeserializer和CustomCreationConverter。因此,问题显然出现在太大无法在此处发布的类型化DataSet中。

编辑2:使用DataSet设计器在Visual Studio中创建的ChildParentDataSet和手动创建的ChildParentDataSet2(如上所示)都会产生相同的序列化JSON:“{"Child":[{"ParentId":1}],"Parent":[{"Id":1}]}"。现在我只需要弄清楚为什么我的DataSetDeserializer对ChildParentDataSet2有效,而对ChildParentDataSet无效。感谢大家迄今为止的帮助!

编辑3:我想我弄清楚了为什么我的CustomCreationConverter对手动编写的ChildParentDataSet2有效,而对使用设计器创建的ChildParentDataSet无效。通过使用CustomCreationConverter,我错过了Newtonsoft在其自己的DataSetConverter类中提供的所有功能。因此,我的当前解决方案是在Github上创建Newtonsoft的DataSetConverter类的分支。这个分支只是在反序列化之前将EnforceConstraints设置为false,然后在反序列化完成后将其设置为true。我还创建了一个pull request,但可能会有性能影响或其他原因,作者可能不想包括我的更改。

英文:

I'm attempting to serialize and deserialize typed DataSets to JSON using C# and the NewtonSoft.Json package. This works fine except for DataSets that include a foreign key constraint and the parent table is added to the designer after the child table. In the auto-generated Designer.cs, the parent table is added to the base DataTableCollection after the child table.

When the DataSet is serialized to JSON the child table is serialized first. Then when deserializing the JSON back to an instance of the typed DataSet, an InvalidConstraintException is thrown ("ForeignKeyConstraint requires the child key values to exist in the parent table").

To setup a test scenario, I created a very simple typed DataSet named ChildParentDataSet. It contains two tables. The first table is named Child and it has one column named ParentId. The second table is named Parent and it has one column named Id. A foreign key is created between Parent.Id and Child.ParentId. It's important for the Parent table to be added in the designer after the Child table. This code results in an InvalidConstraintException on the last line:

ChildParentDataSet ds = new ChildParentDataSet();
ds.Parent.Rows.Add(1);
ds.Child.Rows.Add(1);
string json = Newtonsoft.Json.JsonConvert.SerializeObject(ds);
ChildParentDataSet? ds2 = Newtonsoft.Json.JsonConvert.DeserializeObject&lt;ChildParentDataSet&gt;(json);

My preferred approach is to handle this during deserialization by implementing my own CustomCreationConverter that initializes DataSet.EnforceConstraints to false prior to performing the deserialization, and then when deserialization is complete I would set EnforceConstraints back to true. Here are the classes that I hoped would accomplish this:

public class DataSetDeserializer
{
   public static T DeserializeDataSet&lt;T&gt;(string json)
   {
      var ds = Newtonsoft.Json.JsonConvert.DeserializeObject&lt;T&gt;(json, new DataSetConverter&lt;T&gt;());
      System.Data.DataSet? dataSet = (System.Data.DataSet?)(Object?)ds;
      dataSet.EnforceConstraints = true;
      return ds;
   }

   private class DataSetConverter&lt;T&gt; : Newtonsoft.Json.Converters.CustomCreationConverter&lt;T&gt;
   {
      public override T Create(Type objectType)
      {
         var ds = Activator.CreateInstance(objectType);
         System.Data.DataSet? dataSet = (System.Data.DataSet?)ds;
         dataSet.EnforceConstraints = false;
         return (T)ds;
      }
   }
}

Then I changed the last line of my test code to use the DataSetDeserializer class rather than Newtonsoft for deserialization:

ChildParentDataSet? ds2 = DataSetDeserializer.DeserializeDataSet&lt;ChildParentDataSet&gt;(json);

When running this code a Newtonsoft.Json.JsonSerializationException is thrown from the call to DeserializeObject. The exception message is "Cannot populate JSON object onto type 'ChildParentDataSet'. Path 'Child', line 1, position 9.'" I haven't been able to figure this out.

Another option I considered is to handle this during serialization rather than deserialization. I implemented a method that sorts the tables in a DataSet such that the highest-level parent tables would be first and the lowest-level child tables would be last. Then I serialize each table one at a time in the correct order and concatenate all the JSON strings. That works, but critically it doesn't handle the case when a table is a parent and child of itself via a self-referential foreign key. I can probably figure out how to handle that scenario but it would be substantially more difficult. Plus my current implementation is quite inefficient versus serializing an entire dataset with one call.

One option that would work but I really don't want to do is to use XML instead of JSON. It would certainly be easier since DataSets have a GetXml function, but XML is much more verbose and I'm passing this data over the wire via gRPC. Plus it would require a very complicated redeployment of many clients and services.

I'm certainly open to suggestions. I'm sure there are better alternatives that I haven't considered.

Edit: @Serge suggested that I post the code of ChildParentDataSet. Unfortunately that's a lot of auto-generated code that is way too big to post here, but he then suggested that I create something equivalent in code. I did so and here it is:

internal class ChildParentDataSet2: DataSet
{
   public DataTable Child { get; set; }
   public DataTable Parent { get; set; }

   public ChildParentDataSet2()
   {
      this.Child = this.Tables.Add(&quot;Child&quot;);
      DataColumn childColumn = this.Child.Columns.Add(&quot;ParentId&quot;, typeof(int));
      this.Parent = this.Tables.Add(&quot;Parent&quot;);
      DataColumn parentColumn = this.Parent.Columns.Add(&quot;Id&quot;, typeof(int));
      this.Parent.PrimaryKey = new DataColumn[] { parentColumn };
      this.Child.ParentRelations.Add(parentColumn, childColumn);
   }
}

Strangely when using this class instead of the auto-generated one, I can deserialize it successfully when using my DataSetDeserializer and CustomCreationConverter. So the problem apparently lies in the typed dataset which is too large to post here.

Edit 2: The ChildParentDataSet (created using the DataSet designer in Visual Studio) and the ChildParentDataSet2 (created manually as shown above) both produce the same serialized JSON: {&quot;Child&quot;:[{&quot;ParentId&quot;:1}],&quot;Parent&quot;:[{&quot;Id&quot;:1}]}. Now I just need to figure out why my DataSetDeserializer works fine with ChildParentDataSet2 but not with ChildParentDataSet. Thanks everyone for your help so far!

Edit 3: I think I figured out why my CustomCreationConverter worked for the ChildParentDataSet2 that I wrote manually and not for the ChildParentDataSet that I created via the designer. By using a CustomCreationConverter I was missing out on all the functionality that Newtonsoft provides in its own DataSetConverter class. So my current solution is to create a fork of Newtonsoft's DataSetConverter class on Github. That fork simply sets EnforceConstraints to false prior to deserialization and sets it back to true after. I also created a pull request but there might be performance implications or other reasons that the author won't want to include my changes.

答案1

得分: 1

我建议修改你的 DataSet 构造函数,使其适应一个 JSON 字符串。因为 JSON 字符串不包含任何关系,所以你将需要每次都添加它们。

public class ChildParentDataSet2 : DataSet
{
    public DataTable Child { get; set; }
    public DataTable Parent { get; set; }

    private void CreateDataSet(DataTable parent, DataTable child)
    {
        Tables.Add(child);
        this.Child = Tables[0];
        Tables.Add(parent);
        this.Parent = Tables[1];

        if (this.Parent.Columns.Count == 0)
        {
            this.Parent.Columns.Add("Id", typeof(int));
            this.Child.Columns.Add("ParentId", typeof(int));
        }

        var parentColumn = this.Parent.Columns["Id"];
        var childColumn = this.Child.Columns["ParentId"];

        this.Parent.PrimaryKey = new DataColumn[] { parentColumn };
        this.Child.ParentRelations.Add(parentColumn, childColumn);

        this.Parent.TableName = "Parent";
        this.Child.TableName = "Child";
    }

    public ChildParentDataSet2(string json)
    {      
        var jObj = JObject.Parse(json);
        CreateDataSet(jObj["Parent"].ToObject<DataTable>(), jObj["Child"].ToObject<DataTable>());
    }

    public ChildParentDataSet2(JObject jObj)
    {
        CreateDataSet(jObj["Parent"].ToObject<DataTable>(), jObj["Child"].ToObject<DataTable>());
    }

    public ChildParentDataSet2()
    {
        CreateDataSet(new DataTable(), new DataTable());
    }
}

测试代码:

var ds = new ChildParentDataSet2();
ds.Parent.Rows.Add(1);
ds.Child.Rows Add(1);

string json = Newtonsoft.Json.JsonConvert.SerializeObject(ds);

var ds2 = new ChildParentDataSet2(json);

ds2.Child.Rows.Add(3); // 导致错误的键冲突

如果你愿意,你可以创建一个自定义转换器:

var ds2 = JsonConvert.DeserializeObject<ChildParentDataSet2>(json, new DataSetConverter());

public class DataSetConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(DataSet) || objectType == typeof(ChildParentDataSet2));
    }

    public override ChildParentDataSet2 ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var jObj = JObject.Load(reader);

        return new ChildParentDataSet2(jObj["Parent"].ToObject<DataTable>(), jObj["Child"].ToObject<DataTable>());
    }

    public override bool CanWrite
    {
        get { return false; }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

对于这个转换器,你需要添加以下构造函数(或替换现有的):

public ChildParentDataSet2(DataTable parent, DataTable child)
{
    CreateDataSet(parent, child);
}
英文:

I recommend to change your DataSet constructor, adapting it to a json string. Since a json string doesn't contain any relations, you will have to add them each time.

public class ChildParentDataSet2 : DataSet
{
	public DataTable Child { get; set; }

	public DataTable Parent { get; set; }

	private void CreateDataSet(DataTable parent, DataTable child)
	{
		Tables.Add(child);
		this.Child = Tables[0];
		Tables.Add(parent);
		this.Parent = Tables[1];

		if (this.Parent.Columns.Count == 0)
		{
			this.Parent.Columns.Add(&quot;Id&quot;, typeof(int));
			this.Child.Columns.Add(&quot;ParentId&quot;, typeof(int));
		}

		var parentColumn = this.Parent.Columns[&quot;Id&quot;];
		var childColumn = this.Child.Columns[&quot;ParentId&quot;];

		this.Parent.PrimaryKey = new DataColumn[] { parentColumn };
		this.Child.ParentRelations.Add(parentColumn, childColumn);

		this.Parent.TableName = &quot;Parent&quot;;
		this.Child.TableName = &quot;Child&quot;;
	}

    public ChildParentDataSet2(string json)
	{      
		    var jObj=JObject.Parse(json);
			CreateDataSet(jObj[&quot;Parent&quot;].ToObject&lt;DataTable&gt;(), jObj[&quot;Child&quot;].ToObject&lt;DataTable&gt;());
	}

	public ChildParentDataSet2(JObject jObj)
	{
	    CreateDataSet(jObj[&quot;Parent&quot;].ToObject&lt;DataTable&gt;(), jObj[&quot;Child&quot;].ToObject&lt;DataTable&gt;());
	}
	public ChildParentDataSet2()
	{
		CreateDataSet(new DataTable(), new DataTable());
	}
}

code for a test

	var ds = new ChildParentDataSet2();
	ds.Parent.Rows.Add(1);
	ds.Child.Rows.Add(1);

	string json = Newtonsoft.Json.JsonConvert.SerializeObject(ds);

	var ds2 = new ChildParentDataSet2(json);
	
     ds2.Child.Rows.Add(3); // causes error key violation

If you like , you can create a custom converter

var ds2 = JsonConvert.DeserializeObject&lt;ChildParentDataSet2&gt;(json, new DataSetConverter());


public class DataSetConverter : JsonConverter
{
	public override bool CanConvert(Type objectType)
	{
		return (objectType == typeof(DataSet) || objectType == typeof(ChildParentDataSet2));
	}

	public override ChildParentDataSet2 ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
	{
		var jObj = JObject.Load(reader);

		return new ChildParentDataSet2(jObj[&quot;Parent&quot;].ToObject&lt;DataTable&gt;(), jObj[&quot;Child&quot;].ToObject&lt;DataTable&gt;());
	}
	public override bool CanWrite
	{
		get { return false; }
	}
	public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
	{
		throw new NotImplementedException();
	}
}

for this converter you will need to add this constructor (or replace existing)

    public ChildParentDataSet2(DataTable parent, DataTable child)
	{
		CreateDataSet(parent, child);
	}

答案2

得分: 0

以下是翻译好的部分:

"i'm doing something similar in aspnet using dataset to json for angularjs and then back json to dataset. In case you have a foreign key constraints you need just make json conversion in the correct order. If table2 is a child of table1 it has to be oredered table1, table2 in json string. Then you put in data for each table. This is important for JsonToData conversion.. like that:

.cs
dsjson = JsonConverter.DataSetToJson(ds);
.js on init:
$scope.data = {"table1": [],"table2": []} // in correct order
then fill:
var json = jQuery.parseJSON(dsjson);
scope.data.table1= json.table1; //
scope.data.table2= json.table2;

.cs and when you go back to dataset, also correct order
jds = JsonConverter.JsonToData<Ds_xxx>(json);
var ds = new Ds_xxx()
ds.Merge(jds.table1);
ds.Merge(jds.table2);"

请注意,代码部分没有翻译。

英文:

i'm doing something similar in aspnet using dataset to json for angularjs and then back json to dataset. In case you have a foreign key constraints you need just make json conversion in the correct order. If table2 is a child of table1 it has to be oredered table1, table2 in json string. Then you put in data for each table. This is important for JsonToData conversion.. like that:

.cs 
  dsjson = JsonConverter.DataSetToJson(ds);    
.js on init:
      $scope.data = {&quot;table1&quot;: [],&quot;table2&quot;: []} // in correct order
    then fill:
      var json = jQuery.parseJSON(dsjson);
      scope.data.table1= json.table1; // 
      scope.data.table2= json.table2;
    
.cs and when you go back to dataset, also correct order
    jds = JsonConverter.JsonToData&lt;Ds_xxx&gt;(json);
    var ds = new Ds_xxx()
    ds.Merge(jds.table1);
    ds.Merge(jds.table2);

huangapple
  • 本文由 发表于 2023年2月16日 04:13:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/75465016.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定