JSON'da Çift Yönlü İlişki Desteği

Yayınlanan: 2022-03-11

Hiç çift yönlü bir ilişkiye (yani döngüsel referans) sahip varlıkları içeren bir JSON veri yapısı oluşturmaya çalıştınız mı? Varsa, muhtemelen “Yakalanmayan TypeError: Döngüsel yapıyı JSON'a dönüştürme” satırlarında bir JavaScript hatası görmüşsünüzdür. Veya Jackson kütüphanesini kullanan bir Java geliştiricisiyseniz, “JSON yazılamaz: Sonsuz özyineleme (StackOverflowError) ile kök neden java.lang.StackOverflowError” ile karşılaşmış olabilirsiniz.

JSON Çift Yönlü İlişki Mücadelesi

Bu makale, bu hatalara yol açmadan çift yönlü bir ilişki içeren JSON yapıları oluşturmaya yönelik sağlam bir çalışma yaklaşımı sağlar.

Çoğu zaman, bu soruna sunulan çözümler, temelde yan adım olan, ancak sorunu gerçekten ele almayan geçici çözümler gerektirir. Örnekler arasında @JsonManagedReference ve @JsonBackReference (serileştirmeden geriye referansı atlar) gibi Jackson ek açıklama türlerinin kullanılması veya ilişkinin taraflarından birini yok saymak için @JsonIgnore kullanılması yer alır. Alternatif olarak, verilerdeki bu tür çift yönlü ilişkileri veya döngüsel bağımlılığı yok sayan özel serileştirme kodu geliştirilebilir.

Ancak çift yönlü ilişkinin her iki tarafını da göz ardı etmek veya ihmal etmek istemiyoruz. Herhangi bir hata oluşturmadan her iki yönde de korumak istiyoruz. Gerçek bir çözüm , JSON'da döngüsel bağımlılıklara izin vermeli ve geliştiricinin, bunları düzeltmek için ek eylemlerde bulunmadan bunları düşünmeyi bırakmasına izin vermelidir. Bu makale, günümüzün ön uç geliştiricileri için herhangi bir standart ipucu ve uygulamaya faydalı bir ek olarak hizmet edebilecek, bunu yapmak için pratik ve anlaşılır bir teknik sunmaktadır.

Basit Bir Çift Yönlü İlişki Örneği

Bu çift yönlü ilişki (diğer adıyla döngüsel bağımlılık) sorununun ortaya çıktığı yaygın bir durum, çocukları olan (başvuruda bulunduğu) bir üst nesne olduğunda ve bu alt nesneler, sırayla, üstlerine referansları korumak istediğinde ortaya çıkar. İşte basit bir örnek:

 var obj = { "name": "I'm parent" } obj.children = [ { "name": "I'm first child", "parent": obj }, { "name": "I'm second child", "parent": obj } ]

Yukarıdaki parent nesneyi JSON'a dönüştürmeye çalışırsanız (örneğin, var parentJson = JSON.stringify(parent); olduğu gibi stringify yöntemini kullanarak), Uncaught TypeError: Converting dairesel yapıyı JSON'a dönüştürme istisnası atılır.

Yukarıda tartışılan tekniklerden birini kullanabilsek de (örneğin @JsonIgnore gibi ek açıklamaları kullanmak gibi) veya ebeveynlere yapılan yukarıdaki referansları çocuklardan basitçe kaldırabilsek de, bunlar sorunu çözmekten ziyade kaçınmanın yollarıdır. Gerçekten istediğimiz, her iki yönlü ilişkiyi koruyan ve herhangi bir istisna atmadan JSON'a dönüştürebileceğimiz bir JSON yapısıdır.

Çözüme Doğru İlerlemek

Çözüme yönelik potansiyel olarak açık bir adım, her nesneye bir tür nesne kimliği eklemek ve ardından çocukların üst nesneye referanslarını, üst nesnenin kimliğine yapılan referanslarla değiştirmektir. Örneğin:

 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 } ]

Bu yaklaşım, çift yönlü bir ilişkiden veya döngüsel referanstan kaynaklanan istisnalardan kesinlikle kaçınacaktır. Ancak hala bir sorun var ve bu referansları nasıl seri hale getireceğimizi ve seri durumdan çıkaracağımızı düşündüğümüzde bu sorun ortaya çıkıyor.

Sorun şu ki, yukarıdaki örneği kullanarak, “100” değerine yapılan her başvurunun ana nesneye atıfta bulunduğunu bilmemiz gerekir (çünkü bu onun id 'sidir). Bu, "100" değerine sahip tek özelliğin parent özellik olduğu yukarıdaki örnekte gayet iyi sonuç verecektir. Peki ya “100” değerine sahip başka bir mülk eklersek? Örneğin:

 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" değerine yapılan herhangi bir referansın bir nesneye atıfta bulunduğunu varsayarsak, serileştirme/seri hale getirme kodumuzun, parent "100" değerine atıfta bulunduğunda, bunun ana nesnenin id atıfta bulunduğunu bilmesinin bir yolu olmayacaktır. priority , ana nesnenin id başvurmayan “100” değerine başvurur (ve priority aynı zamanda üst nesnenin id gönderme yaptığını düşüneceğinden, değerini hatalı bir şekilde üst nesneye bir başvuruyla değiştirir).

Bu noktada şunu sorabilirsiniz: “Bekle, bariz bir çözümü kaçırıyorsun. Bir nesne kimliğine atıfta bulunduğunu belirlemek için özellik değerini kullanmak yerine, neden yalnızca özellik adını kullanmıyorsunuz?” Gerçekten de, bu bir seçenek, ancak çok sınırlayıcı bir seçenek. Bu, her zaman diğer nesnelere ("ebeveyn", "alt", "sonraki" gibi adlar) atıfta bulunduğu varsayılan "ayrılmış" özellik adlarının bir listesini önceden belirlememiz gerektiği anlamına gelir. Bu, diğer nesnelere yapılan başvurular için yalnızca bu özellik adlarının kullanılabileceği anlamına gelir ve ayrıca bu özellik adlarının her zaman diğer nesnelere başvurular olarak ele alınacağı anlamına gelir. Bu nedenle, çoğu durumda bu uygun bir alternatif değildir.

Bu nedenle, özellik değerlerini nesne referansları olarak tanımaya devam etmemiz gerekiyor gibi görünüyor. Ancak bu, diğer tüm özellik değerlerinden benzersiz olduklarının garanti edilmesi için bu değerlere ihtiyacımız olacağı anlamına gelir. Globally Unique Identifiers (GUID'ler) kullanarak benzersiz değerlere olan ihtiyacı ele alabiliriz. Örneğin:

 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 } ]

Yani bu işe yaramalı, değil mi?

Evet.

Ancak…

Tam Otomatik Çözüm

Asıl meydan okumamızı hatırla. Herhangi bir istisna oluşturmadan JSON ile/JSON arasında çift yönlü bir ilişkisi olan nesneleri seri hale getirebilmek ve seri durumdan çıkarabilmek istedik. Yukarıdaki çözüm bunu gerçekleştirirken, bunu (a) her nesneye bir tür benzersiz kimlik alanı eklememizi ve (b) her nesne referansını karşılık gelen benzersiz kimlikle değiştirmemizi isteyerek yapar. Bu işe yarayacaktır, ancak nesnelerimizi bu şekilde “manuel” olarak değiştirmemize gerek kalmadan mevcut nesne referanslarımızla otomatik olarak çalışacak bir çözümü tercih ederiz.

İdeal olarak, bir dizi nesneyi (herhangi bir keyfi özellik ve nesne referansı içeren) seri hale getirici ve seri hale getirici aracılığıyla (çift yönlü bir ilişkiye dayalı herhangi bir istisna oluşturmadan) geçirebilmek ve seri hale getirici tarafından oluşturulan nesnelerin tam olarak eşleşmesini sağlamak istiyoruz. serileştiriciye beslenen nesneler.

Yaklaşımımız, serileştiricimizin her nesneye otomatik olarak benzersiz bir kimlik (GUID kullanarak) oluşturmasını ve eklemesini sağlamaktır. Daha sonra herhangi bir nesne referansını o nesnenin GUID'si ile değiştirir. (Seri hale getiricinin bu kimlikler için de bazı benzersiz özellik adları kullanması gerekeceğini unutmayın; örneğimizde, mülk adının başına "@" eklemek, benzersiz olduğundan emin olmak için muhtemelen yeterli olduğundan @id kullanıyoruz.) Seri hale getirici daha sonra bir nesne kimliğine karşılık gelen herhangi bir GUID'yi o nesneye bir referansla değiştirir (seri hale getiricinin seri hale getirici tarafından oluşturulan GUID'leri de seri durumdan çıkarılmış nesnelerden kaldıracağını ve böylece onları tam olarak ilk durumlarına döndüreceğini unutmayın).

Örneğimize dönersek, aşağıdaki nesne grubunu serileştiricimize olduğu gibi beslemek istiyoruz:

 var obj = { "name": "I'm parent" } obj.children = [ { "name": "I'm first child", "parent": obj }, { "name": "I'm second child", "parent": obj } ]

Daha sonra seri hale getiricinin aşağıdakine benzer bir JSON yapısı oluşturmasını bekleriz:

 { "@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" }, ] }

(Herhangi bir JSON nesnesini güzelleştirmek için bir JSON biçimlendirici aracı kullanabilirsiniz.)

Ardından, yukarıdaki JSON'u seri durumdan çıkarıcıya beslemek, orijinal nesne kümesini oluşturur (yani, ana nesne ve iki çocuğu, birbirine uygun şekilde atıfta bulunur).

Artık ne yapmak istediğimizi ve nasıl yapmak istediğimizi bildiğimize göre, uygulayalım.

JavaScript'te Serileştiriciyi Uygulamak

Aşağıda, herhangi bir istisna atmadan iki yönlü bir ilişkiyi düzgün bir şekilde işleyecek bir serileştiricinin çalışan örnek bir JavaScript uygulaması verilmiştir.

 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'te Seri Ayırıcıyı Uygulamak

Aşağıda, herhangi bir istisna atmadan çift yönlü bir ilişkiyi düzgün şekilde idare edecek bir seri kaldırıcının çalışan örnek bir JavaScript uygulaması verilmiştir.

 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"); }

Bu iki yöntemden bir dizi nesneyi (çift yönlü ilişkisi olanlar dahil) geçirmek, esasen bir özdeşlik işlevidir; yani convertToObject(convertToJson(obj)) === obj true olarak değerlendirilir.

Java/Jackson Örneği

Şimdi bu yaklaşımın popüler harici kütüphanelerde nasıl desteklendiğine bakalım. Örneğin, Jackson kütüphanesini kullanarak Java'da nasıl işlendiğini görelim.

 @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; } }

Bu iki Java sınıfı Parent ve Child , bu makalenin başındaki JavaScript örneğindekiyle aynı yapıyı temsil eder. Buradaki ana nokta, Jackson'a bu nesnelerin nasıl seri hale getirileceğini/seri hale getirileceğini söyleyecek olan @JsonIdentityInfo ek açıklamasını kullanmaktır.

Bir örnek görelim:

 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));

Üst örneğin JSON'a serileştirilmesinin bir sonucu olarak, JavaScript örneğindekiyle aynı JSON yapısı döndürülecektir.

 { "@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" }, ] }

Başka Bir Avantaj

JSON'da iki yönlü bir ilişkiyi ele almaya yönelik açıklanan yaklaşım, aynı nesnenin fazla kopyalarını dahil etmek yerine nesnelere yalnızca benzersiz kimlikleriyle başvurmanıza olanak tanıdığından, bir JSON dosyasının boyutunun azaltılmasına yardımcı olmak için de kullanılabilir.

Aşağıdaki örneği göz önünde bulundurun:

 { "@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 dizisinde gösterildiği gibi, JSON'umuza başvurulan nesnelerin ve içeriklerinin kopyaları yerine nesne referanslarını dahil edebiliriz.

Sarmak

Bu çözümle, JSON dosyalarını nesneleriniz ve verileriniz üzerindeki kısıtlamaları en aza indirecek şekilde serileştirirken döngüsel referansla ilgili istisnaları ortadan kaldırabilirsiniz. JSON dosyalarının serileştirilmesini işlemek için kullandığınız kitaplıklarda böyle bir çözüm zaten mevcut değilse, sağlanan örnek uygulamaya göre kendi çözümünüzü uygulayabilirsiniz. Umarım bunu faydalı bulursun.