Перейти к основному содержимому

Джексон - Двунаправленные отношения

· 7 мин. чтения

1. Обзор

В этом уроке мы рассмотрим лучшие способы работы с двунаправленными отношениями в Джексоне .

Мы обсудим проблему бесконечной рекурсии Джексона JSON, затем — увидим, как сериализовать сущности с двунаправленными отношениями, и, наконец, — десериализуем их.

2. Бесконечная рекурсия

Во-первых, давайте взглянем на проблему бесконечной рекурсии Джексона. В следующем примере у нас есть две сущности — « Пользователь » и « Элемент » — с простым отношением «один ко многим » :

Сущность « Пользователь »:

public class User {
public int id;
public String name;
public List<Item> userItems;
}

Сущность « Предмет »:

public class Item {
public int id;
public String itemName;
public User owner;
}

Когда мы попытаемся сериализовать экземпляр « Item », Джексон выдаст исключение JsonMappingException :

@Test(expected = JsonMappingException.class)
public void givenBidirectionRelation_whenSerializing_thenException()
throws JsonProcessingException {

User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);

new ObjectMapper().writeValueAsString(item);
}

Полное исключение :

com.fasterxml.jackson.databind.JsonMappingException:
Infinite recursion (StackOverflowError)
(through reference chain:
org.foreach.jackson.bidirection.Item["owner"]
->org.foreach.jackson.bidirection.User["userItems"]
->java.util.ArrayList[0]
->org.foreach.jackson.bidirection.Item["owner"]
->..

Давайте посмотрим в следующих нескольких разделах, как решить эту проблему.

3. Используйте @JsonManagedReference , @JsonBackReference

Во-первых, давайте аннотируем отношение с помощью @JsonManagedReference , @JsonBackReference , чтобы позволить Джексону лучше обрабатывать отношение:

Вот сущность « Пользователь »:

public class User {
public int id;
public String name;

@JsonManagedReference
public List<Item> userItems;
}

И « Товар »:

public class Item {
public int id;
public String itemName;

@JsonBackReference
public User owner;
}

Давайте теперь протестируем новые объекты:

@Test
public void givenBidirectionRelation_whenUsingJacksonReferenceAnnotationWithSerialization_thenCorrect() throws JsonProcessingException {
final User user = new User(1, "John");
final Item item = new Item(2, "book", user);
user.addItem(item);

final String itemJson = new ObjectMapper().writeValueAsString(item);
final String userJson = new ObjectMapper().writeValueAsString(user);

assertThat(itemJson, containsString("book"));
assertThat(itemJson, not(containsString("John")));

assertThat(userJson, containsString("John"));
assertThat(userJson, containsString("userItems"));
assertThat(userJson, containsString("book"));
}

Вот результат сериализации объекта Item:

{
"id":2,
"itemName":"book"
}

А вот результат сериализации объекта User:

{
"id":1,
"name":"John",
"userItems":[{
"id":2,
"itemName":"book"}]
}

Обратите внимание, что:

  • @JsonManagedReference — это прямая часть ссылки, которая обычно сериализуется.
  • @JsonBackReference — это задняя часть ссылки — она будет исключена из сериализации.
  • Сериализованный объект Item не содержит ссылки на объект User .

Также обратите внимание, что мы не можем переключаться между аннотациями. Для сериализации будет работать следующее:

@JsonBackReference
public List<Item> userItems;

@JsonManagedReference
public User owner;

Но вызовет исключение, когда мы попытаемся десериализовать объект, поскольку @JsonBackReference нельзя использовать в коллекции.

Если мы хотим, чтобы сериализованный объект Item содержал ссылку на пользователя, нам нужно использовать @JsonIdentityInfo . Мы рассмотрим это в следующем разделе.

4. Используйте @JsonIdentityInfo

Теперь давайте посмотрим, как помочь с сериализацией сущностей с двунаправленными отношениями, используя @JsonIdentityInfo .

Мы добавляем аннотацию уровня класса к нашей сущности « Пользователь »:

@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class User { ... }

И к сущности « Item »:

@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Item { ... }

Время для теста:

@Test
public void givenBidirectionRelation_whenUsingJsonIdentityInfo_thenCorrect()
throws JsonProcessingException {

User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);

String result = new ObjectMapper().writeValueAsString(item);

assertThat(result, containsString("book"));
assertThat(result, containsString("John"));
assertThat(result, containsString("userItems"));
}

Вот результат сериализации:

{
"id":2,
"itemName":"book",
"owner":
{
"id":1,
"name":"John",
"userItems":[2]
}
}

5. Используйте @JsonIgnore

В качестве альтернативы мы также можем использовать аннотацию @JsonIgnore , чтобы просто игнорировать одну из сторон отношения , тем самым разорвав цепочку.

В следующем примере — мы предотвратим бесконечную рекурсию, игнорируя свойство « Пользователь » « userItems » из сериализации:

Вот объект « Пользователь »:

public class User {
public int id;
public String name;

@JsonIgnore
public List<Item> userItems;
}

И вот наш тест:

@Test
public void givenBidirectionRelation_whenUsingJsonIgnore_thenCorrect()
throws JsonProcessingException {

User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);

String result = new ObjectMapper().writeValueAsString(item);

assertThat(result, containsString("book"));
assertThat(result, containsString("John"));
assertThat(result, not(containsString("userItems")));
}

И вот результат сериализации:

{
"id":2,
"itemName":"book",
"owner":
{
"id":1,
"name":"John"
}
}

6. Используйте @JsonView

Мы также можем использовать более новую аннотацию @JsonView , чтобы исключить одну сторону отношения.

В следующем примере мы используем два представления JSON — Public и Internal , где Internal расширяет Public :

public class Views {
public static class Public {}

public static class Internal extends Public {}
}

Мы включим все поля « Пользователь » и « Элемент » в общий вид , кроме поля « Пользователь », userItems , который будет включен во внутренний вид: ``

Вот наша сущность « Пользователь »:

public class User {
@JsonView(Views.Public.class)
public int id;

@JsonView(Views.Public.class)
public String name;

@JsonView(Views.Internal.class)
public List<Item> userItems;
}

А вот и наша сущность « Item »:

public class Item {
@JsonView(Views.Public.class)
public int id;

@JsonView(Views.Public.class)
public String itemName;

@JsonView(Views.Public.class)
public User owner;
}

Когда мы сериализуем с помощью Public view, все работает правильно, потому что мы исключили userItems из сериализации:

@Test
public void givenBidirectionRelation_whenUsingPublicJsonView_thenCorrect()
throws JsonProcessingException {

User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);

String result = new ObjectMapper().writerWithView(Views.Public.class)
.writeValueAsString(item);

assertThat(result, containsString("book"));
assertThat(result, containsString("John"));
assertThat(result, not(containsString("userItems")));
}

Но если мы сериализуем с использованием внутреннего представления, будет выброшено JsonMappingException , потому что включены все поля:

@Test(expected = JsonMappingException.class)
public void givenBidirectionRelation_whenUsingInternalJsonView_thenException()
throws JsonProcessingException {

User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);

new ObjectMapper()
.writerWithView(Views.Internal.class)
.writeValueAsString(item);
}

7. Используйте собственный сериализатор

Далее — давайте посмотрим, как сериализовать сущности с двунаправленными отношениями с помощью пользовательского сериализатора.

В следующем примере мы будем использовать пользовательский сериализатор для сериализации свойства « Пользователь » « userItems »:

Вот сущность « Пользователь »:

public class User {
public int id;
public String name;

@JsonSerialize(using = CustomListSerializer.class)
public List<Item> userItems;
}

А вот и « CustomListSerializer »:

public class CustomListSerializer extends StdSerializer<List<Item>>{

public CustomListSerializer() {
this(null);
}

public CustomListSerializer(Class<List> t) {
super(t);
}

@Override
public void serialize(
List<Item> items,
JsonGenerator generator,
SerializerProvider provider)
throws IOException, JsonProcessingException {

List<Integer> ids = new ArrayList<>();
for (Item item : items) {
ids.add(item.id);
}
generator.writeObject(ids);
}
}

Теперь давайте протестируем сериализатор и посмотрим, какой результат получается:

@Test
public void givenBidirectionRelation_whenUsingCustomSerializer_thenCorrect()
throws JsonProcessingException {
User user = new User(1, "John");
Item item = new Item(2, "book", user);
user.addItem(item);

String result = new ObjectMapper().writeValueAsString(item);

assertThat(result, containsString("book"));
assertThat(result, containsString("John"));
assertThat(result, containsString("userItems"));
}

И окончательный результат сериализации с помощью пользовательского сериализатора:

{
"id":2,
"itemName":"book",
"owner":
{
"id":1,
"name":"John",
"userItems":[2]
}
}

8. Десериализация с помощью @JsonIdentityInfo

Теперь давайте посмотрим, как десериализовать сущности с двунаправленными отношениями, используя @JsonIdentityInfo .

Вот объект « Пользователь »:

@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class User { ... }

И объект « Item »:

@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Item { ... }

Давайте теперь напишем быстрый тест — начиная с некоторых данных JSON, которые мы хотим проанализировать, и заканчивая правильно построенным объектом:

@Test
public void givenBidirectionRelation_whenDeserializingWithIdentity_thenCorrect()
throws JsonProcessingException, IOException {
String json =
"{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}";

ItemWithIdentity item
= new ObjectMapper().readerFor(ItemWithIdentity.class).readValue(json);

assertEquals(2, item.id);
assertEquals("book", item.itemName);
assertEquals("John", item.owner.name);
}

9. Используйте пользовательский десериализатор

Наконец, давайте десериализуем сущности с двунаправленной связью, используя пользовательский десериализатор.

В следующем примере мы будем использовать пользовательский десериализатор для анализа свойства « Пользователь » « userItems »:

Вот объект « Пользователь »:

public class User {
public int id;
public String name;

@JsonDeserialize(using = CustomListDeserializer.class)
public List<Item> userItems;
}

А вот и наш « CustomListDeserializer »:

public class CustomListDeserializer extends StdDeserializer<List<Item>>{

public CustomListDeserializer() {
this(null);
}

public CustomListDeserializer(Class<?> vc) {
super(vc);
}

@Override
public List<Item> deserialize(
JsonParser jsonparser,
DeserializationContext context)
throws IOException, JsonProcessingException {

return new ArrayList<>();
}
}

И простой тест:

@Test
public void givenBidirectionRelation_whenUsingCustomDeserializer_thenCorrect()
throws JsonProcessingException, IOException {
String json =
"{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}";

Item item = new ObjectMapper().readerFor(Item.class).readValue(json);

assertEquals(2, item.id);
assertEquals("book", item.itemName);
assertEquals("John", item.owner.name);
}

10. Заключение

В этом руководстве мы продемонстрировали, как сериализовать/десериализовать сущности с двунаправленными отношениями с помощью Jackson.

Реализацию всех этих примеров и фрагментов кода можно найти в нашем проекте GitHub — это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.