本文原文:Bidirectional Relationship Support in JSON
本周一Irene发来这篇文章,无奈这周项目有点赶,最初只是粗略的看了一下,发现这是个之前没遇到过的好玩的问题,于是只能在公交上边搜索遍看看里面具体内容了,今天忙里偷闲把文章分享给大家。英语能力有限,只能把自己理解的一点点分享出来,有能力者可以去本文首页看英文原文。
曾经试图创建过一个包含双向关联(即循环引用)的实体的JSON结构么?若是你经历过,你可能会看到一行名为“Uncaught TypeError: Converting circular structure to JSON”
的JavaScript错误。若你是一名使用Jackson库的Java开发者,你可能会遭遇到“Could not write JSON: Infinite recursion (StackOverflowError) with root cause java.lang.StackOverflowError”
这样的错误。
本文提供了一种稳健的而且不会导致这些错误的工作方法来创建包含双向关联的JSON结构。
通常,那些提出的关于这个问题的解决方案基本都是避开的方法,不是真正的解决此问题的方法。比如包括使用Jackson注解的方式,如 @JsonManagedReference
and @JsonBackReference
(它只是简单的在序列化时忽略其后面的引用)或者使用@JsonIgnore
简单省略引用的中的一侧。或者,可以开发忽略数据中任何这种双向引用关系或者循环依赖的定制的序列化代码。
但是,我们不想忽略双向关联中的任何一方。我们想保持这种双向关联同时又不产生任何错误。一个真正的解决方案应该是允许JSON中存在这种循环依赖,同时让开发者们不用思考采用其他方式解决它们。本文为此提供了一个实用而直接的技术,这可以为今天的前端开发人员提供一个有用的补充适用于任何标准的技巧和实践。
出现这种双向关联(也称为循环依赖)问题的常见情况是,当存在具有子对象的父对象,并且那些子对象又要保持对其父对象的引用。这里有一个简单的例子:
var obj = {
"name": "I'm parent"
}
obj.children = [
{
"name": "I'm first child",
"parent": obj
},
{
"name": "I'm second child",
"parent": obj
}
]
效果图
如果你试图将上面的父对象(obj)转化为JSON(如,使用stringify
方法,就像var parentJson = JSON.stringify(obj);
),将会抛出异常Uncaught TypeError: Converting circular structure to JSON will be thrown.
。(各位可以自己在浏览器控制台运行看效果。)
虽然我们可以使用上面讨论的技术之一(例如使用注解@JsonIgnore
),或者我们可以简单地从子节点中删除上面对父节点的引用,但这些是避免而不是解决问题的方法。我们真正想要的是一个生成的JSON结构,它维护每个双向关系,并且我们可以转换为JSON而不抛出任何异常。
解决方法中显而易见的一步是向每个对象中添加某种形式的对象ID,然后使用对父对象的id的引用替换子对父对象的引用。例如:
var obj = {
"id": 100,
"name": "I'm parent"
}
obj.children = [
{
"id": 101,
"name": "I'm first child",
"parent": 100
},
{
"id": 102,
"name": "I'm second child",
"parent": 100
}
]
这种方法肯定会避免由双向关系或循环引用引起的任何异常。但是仍然有一个问题,当我们考虑如何对这些引用进行序列化和反序列化时,这个问题变得明显。
问题是我们需要知道,使用上面的例子,每个对值“100”的引用是指父对象(因为它是id
)。这将很好地运行在上面这种仅有唯一的属性值“100”对应父对象的属性的示例中。但是如果我们添加另一个值为“100”的属性呢?例如:
obj.children = [
{
"id": 101,
"name": "I'm first child",
"priority": 100, // This is NOT referencing object ID "100"
"parent": 100 // This IS referencing object ID "100"
},
{
"id": 102,
"name": "I'm second child",
"priority": 200,
"parent": 100
}
]
如果我们假设对值“100”的任何引用都引用自一个对象,那么我们的序列化/反序列化代码将无法知道parent
引用值“100”时是在引用父对象的id
,与此同时priority
引用值“100”时并不是引用父对象的id
(因为它会认为priority
也引用父对象的id,它将错误的将值替换为一个对父对象的引用)。
你可能会问这样一点,“等等,你遗漏了一个显而易见的解决方案。与其是使用属性值来确定它是引用自一个对象id呢,为什么不使用属性的名字呢?”的确,这是一个选项,但是个非常有局限性的选项。这将意味着我们需要预先指定一个 “reserved” property names的列表,这些名称总是被假定为引用其他对象(例如“parent”,“child”,“next”等)。这将意味着只有那些属性名称可以用于引用其他对象,并且还意味着这些属性名称将始终被视为对其他对象的引用。因此,这在大多数情况下不是可行的替代方案。
所以看起来我们需要坚持把属性值作为对象引用。但是,这意味着我们需要将这些值保证是与所有其他属性值 相比是唯一的。我们可以通过使用全局唯一标识符(GUID)来满足对唯一值的需求。例如:
var obj = {
"id": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc",
"name": "I'm parent"
}
obj.children = [
{
"id": "6616c598-0a0a-8263-7a56-fb0c0e16225a",
"name": "I'm first child",
"priority": 100,
"parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc" // matches unique parent id
},
{
"id": "940e60e4-9497-7c0d-3467-297ff8bb9ef2",
"name": "I'm second child",
"priority": 200,
"parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc" // matches unique parent id
}
]
所以这应该能运行了,对吧?
是的,
但。。。
记住我们最初的挑战。我们希望能够序列化和反序列化那些有双向关联的JSON,同时不会生成任何异常。虽然上述解决方案实现了这一点,但是它通过要求我们(a)向每个对象添加某种形式的唯一ID字段并且(b)用相应的唯一ID 替换每个对象引用来实现。这将会正常工作,但我们更喜欢一个只是自动使用我们现有的对象引用,而不需要我们“手动”修改我们的对象这种方式的解决方案。
理想情况下,我们希望能够通过序列化器和反序列化器(不基于双向关联生成任何异常)传递一组对象(包含任何任意属性和对象引用集),并使反序列化器生成的对象精确匹配被送入序列化器的对象。
我们的方法是让我们的序列化器自动创建和添加一个唯一的ID(使用GUID)到每个对象。然后它用该对象的GUID替换任何对象引用。(请注意,序列化程序还需要为这些ID 使用一些唯一的属性名 ;在我们的示例中,我们使用,@id
因为大概在属性名前加“@
”就足以确保它是唯一的)。然后反序列化器将使用对该对象的引用替换与对象ID相对应的任何GUID(注意,反序列化器还将从反序列化对象中移除序列化器生成的GUID,从而将它们精确地返回到其初始状态)。
所以回到我们的例子中,我们想要将以下一组对象作为我们的序列化器:
var obj = {
"name": "I'm parent"
}
obj.children = [
{
"name": "I'm first child",
"parent": obj
},
{
"name": "I'm second child",
"parent": obj
}
]
然后,我们期望序列化器生成类似于以下内容的JSON结构:
{
"@id": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc",
"name": "I'm parent",
"children": [
{
"@id": "6616c598-0a0a-8263-7a56-fb0c0e16225a",
"name": "I'm first child",
"parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc"
},
{
"@id": "940e60e4-9497-7c0d-3467-297ff8bb9ef2",
"name": "I'm second child",
"parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc"
},
]
}
然后将上述JSON提供给反序列化器将生成原始的对象集合(即,父对象及其两个孩子,正确地引用另一个对象)。
所以现在我们知道我们想要做什么以及我们想要做什么,让我们实现它。
下面是工作示例的JavaScript实现的序列化器将妥善处理双向关联而没有抛出任何异常的。
JavaScript版Serializer
var convertToJson = function(obj) {
// Generate a random value structured as a GUID
var guid = function() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
};
// Check if a value is an object
var isObject = function(value) {
return (typeof value === 'object');
}
// Check if an object is an array
var isArray = function(obj) {
return (Object.prototype.toString.call(obj) === '[object Array]');
}
var convertToJsonHelper = function(obj, key, objects) {
// Initialize objects array and
// put root object into if it exist
if(!objects) {
objects = [];
if (isObject(obj) && (! isArray(obj))) {
obj[key] = guid();
objects.push(obj);
}
}
for (var i in obj) {
// Skip methods
if (!obj.hasOwnProperty(i)) {
continue;
}
if (isObject(obj[i])) {
var objIndex = objects.indexOf(obj[i]);
if(objIndex === -1) {
// Object has not been processed; generate key and continue
// (but don't generate key for arrays!)
if(! isArray(obj)) {
obj[i][key] = guid();
objects.push(obj[i]);
}
// Process child properties
// (note well: recursive call)
convertToJsonHelper(obj[i], key, objects);
} else {
// Current object has already been processed;
// replace it with existing reference
obj[i] = objects[objIndex][key];
}
}
}
return obj;
}
// As discussed above, the serializer needs to use some unique property name for
// the IDs it generates. Here we use "@id" since presumably prepending the "@" to
// the property name is adequate to ensure that it is unique. But any unique
// property name can be used, as long as the same one is used by the serializer
// and deserializer.
//
// Also note that we leave off the 3rd parameter in our call to
// convertToJsonHelper since it will be initialized within that function if it
// is not provided.
return convertToJsonHelper(obj, "@id");
}
下面是一个反序列化器的JavaScript实现示例,它将正确处理双向关系,而不抛出任何异常。
JavaScript版Deserializer
var convertToObject = function(json) {
// Check if an object is an array
var isObject = function(value) {
return (typeof value === 'object');
}
// Iterate object properties and store all reference keys and references
var getKeys = function(obj, key) {
var keys = [];
for (var i in obj) {
// Skip methods
if (!obj.hasOwnProperty(i)) {
continue;
}
if (isObject(obj[i])) {
keys = keys.concat(getKeys(obj[i], key));
} else if (i === key) {
keys.push( { key: obj[key], obj: obj } );
}
}
return keys;
};
var convertToObjectHelper = function(json, key, keys) {
// Store all reference keys and references to object map
if(!keys) {
keys = getKeys(json, key);
var convertedKeys = {};
for(var i = 0; i < keys.length; i++) {
convertedKeys[keys[i].key] = keys[i].obj;
}
keys = convertedKeys;
}
var obj = json;
// Iterate all object properties and object children
// recursively and replace references with real objects
for (var j in obj) {
// Skip methods
if (!obj.hasOwnProperty(j)) {
continue;
}
if (isObject(obj[j])) {
// Property is an object, so process its children
// (note well: recursive call)
convertToObjectHelper(obj[j], key, keys);
} else if( j === key) {
// Remove reference id
delete obj[j];
} else if (keys[obj[j]]) {
// Replace reference with real object
obj[j] = keys[obj[j]];
}
}
return obj;
};
// As discussed above, the serializer needs to use some unique property name for
// the IDs it generates. Here we use "@id" since presumably prepending the "@" to
// the property name is adequate to ensure that it is unique. But any unique
// property name can be used, as long as the same one is used by the serializer
// and deserializer.
//
// Also note that we leave off the 3rd parameter in our call to
// convertToObjectHelper since it will be initialized within that function if it
// is not provided.
return convertToObjectHelper(json, "@id");
}
通过这两种方法传递一组对象(包括具有双向关系的对象)本质上是一个身份函数; 即,convertToObject(convertToJson(obj)) === obj
求值为true。
现在让我们看看这个apporach是如何支持流行的外部库。例如,让我们看看它是如何使用Jackson库在Java中处理哒。
Java / Jackson示例
@JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, property="@id")
public class Parent implements Serializable {
private String name;
private List<Child> children = new ArrayList<>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Child> getChildren() {
return children;
}
public void setChildren(List<Child> children) {
this.children = children;
}
}
@JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, property="@id")
public class Child implements Serializable {
private String name;
private Parent parent;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
}
这两个java类Parent和Child表示与本文开头的JavaScript示例中相同的结构。这里的要点是使用@JsonIdentityInfo
注解,这将告诉Jackson如何序列化/反序列化这些对象。
让我们看一个例子:
Parent parent = new Parent();
parent.setName("I'm parent")
Child child1 = new Child();
child1.setName("I'm first child");
Child child2 = new Child();
child2.setName("I'm second child");
parent.setChildren(Arrays.asList(child1, child2));
由于将父实例序列化为JSON,将返回与JavaScript示例中相同的JSON结构。
JSON结构
{
"@id": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc",
"name": "I'm parent",
"children": [
{
"@id": "6616c598-0a0a-8263-7a56-fb0c0e16225a",
"name": "I'm first child",
"parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc"
},
{
"@id": "940e60e4-9497-7c0d-3467-297ff8bb9ef2",
"name": "I'm second child",
"parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc"
},
]
}
Another Advantage
所描述的在JSON中处理双向关联的方法也可以用于帮助减小JSON文件的大小,因为它使您能够简单地通过其唯一ID引用对象,而不需要包括同一对象的冗余副本。
请考虑以下示例:
优势-示例
{
"@id": "44f47be7-af77-9a5a-8606-a1e6df299ec9",
"id": 1,
"name": "I'm parent",
"children": [
{
"@id": "54f47be7-af77-9a5a-8606-a1e6df299eu8",
"id": 10,
"name": "I'm first child",
"parent": "44f47be7-af77-9a5a-8606-a1e6df299ec9"
},
{
"@id": "98c47be7-af77-9a5a-8606-a1e6df299c7a",
"id": 11,
"name": "I'm second child",
"parent": "44f47be7-af77-9a5a-8606-a1e6df299ec9"
},
{
"@id": "5jo47be7-af77-9a5a-8606-a1e6df2994g2",
"id": 11,
"name": "I'm third child",
"parent": "44f47be7-af77-9a5a-8606-a1e6df299ec9"
}
],
"filteredChildren": [
"54f47be7-af77-9a5a-8606-a1e6df299eu8", "5jo47be7-af77-9a5a-8606-a1e6df2994g2"
]
}
如filteredChildren
数组中所示,我们可以简单地在我们的JSON中包含对象引用,而不是引用对象及其内容的副本。
使用此解决方案,您可以消除循环引用相关的异常,同时以最小化对对象和数据的任何约束的方式序列化JSON文件。如果在您用于处理JSON文件序列化的库中没有这样的解决方案,您可以根据提供的示例实现实现自己的解决方案。希望您觉得这有帮助。
以下内容待续。
1、序列化与反序列化
2、注解简析@JsonManagedReference
、 @JsonBackReference
、@JsonIgnore