Джексон: ссылка на объект как свойство

В моем приложении java spring я работаю с hibernate и jpa, и я использую jackson для заполнения данных в DB.

Вот класс пользователя:

@Data
@Entity
public class User{

    @Id
    @GeneratedValue
    Long id;

    String username;
    String password;
    boolean activated;

    public User(){}
}

а второй класс:

@Entity
@Data
public class Roles {

    @Id
    @GeneratedValue
    Long id;

    @OneToOne
    User user;

    String role;

    public Roles(){}


}

В классе Роли у меня есть свойство User а затем я сделал json файл для хранения данных:

[ {"_class" : "com.example.domains.User", "id": 1, "username": "Admin", "password": "123Admin123","activated":true}
,
  {"_class" : "com.example.domains.Roles", "id": 1,"user":1, "role": "Admin"}]

К сожалению, когда я запускаю приложение, он жалуется:

.RuntimeException: com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.example.domains.User: no int/Int-argument constructor/factory method to deserialize from Number value (1)
 at [Source: N/A; line: -1, column: -1] (through reference chain: com.example.domains.Roles["user"])

Проблема возникает из

{"_class" : "com.example.domains.Roles", "id": 1,"user":1, "role": "Admin"}

и когда я удаляю вышеприведенную строку, приложение работает хорошо.

Я думаю, он жалуется, потому что не может создать экземпляр пользователя. Итак, как я могу это исправить?

Ответ 1

Сделайте себе одолжение и прекратите использовать свои сущности как DTO!

Сущности JPA имеют двунаправленные отношения, объекты JSON этого не делают, я также считаю, что обязанности Entity сильно отличаются от обязанностей DTO, и хотя объединение этих обязанностей в один класс Java возможно, по моему опыту, это очень плохая идея.

Вот несколько причин

  • Вы почти всегда нуждаетесь в большей гибкости в уровне DTO, потому что это часто связано с пользовательским интерфейсом.
  • Вам следует избегать раскрытия первичных ключей из вашей базы данных снаружи, включая собственный пользовательский интерфейс. Мы всегда создаем дополнительный uniqueId (UUID) для каждого публично открытого Entity, первичный ключ остается в БД и используется только для соединений.
  • Вам часто требуется несколько просмотров одного и того же объекта. Или один вид нескольких объектов.
  • Если вам нужно добавить новый объект в отношение с существующим, вам нужно будет найти существующее в базе данных, поэтому публикация нового и старого объектов как единой структуры JSON не имеет преимущества. Вам просто нужен уникальный уникальный существующий, а затем новый.

Многие проблемы, с которыми сталкиваются разработчики JPA, особенно в отношении слияния, связаны с тем, что они получают отдельную сущность после того, как их json был десериализован. Но этот объект обычно не имеет отношений OneToMany (и если это так, то он является родителем, который имеет отношение к дочернему элементу в JSON, но в JPA это дочерняя ссылка на родителя, которая составляет отношение). В большинстве случаев вам всегда нужно загрузить существующую версию объекта из базы данных, а затем скопировать изменения из DTO в объект.

Я много работал с JPA с 2009 года, и я знаю большинство угловых случаев отсоединения и слияния и не имею проблем с использованием Entity как DTO, но я видел путаницу и типы ошибок, которые возникают, когда вы передаете такие код для тех, кто не знаком с JPA. Несколько строк, которые вам нужны для DTO (тем более, что вы уже используете Lombok), настолько просты и позволяют вам гораздо больше гибкости, чем пытаться сохранить несколько файлов и нарушить разделение проблем.

Ответ 2

Jackson предоставляет ObjectIdResolver интерфейс для разрешения объектов из идентификаторов во время де-сериализации.

В вашем случае вы хотите разрешить идентификатор, основанный на JPA/hibernate. Таким образом, вам нужно реализовать настраиваемый преобразователь для разрешения id, вызвав диспетчер сущностей JPA/hierbate.

На высоком уровне ниже приведены шаги:

  • Внедрите пользовательский ObjectIdResolver say JPAEntityResolver (вы можете перейти от SimpleObjectIdResolver). При разрешении объекта он вызовет класс управления сущностью JPA для поиска объекта по заданному идентификатору и области видимости (см. ObjectIdResolver # resolveId java docs)

    //Example only;
    @Component
    @Scope("prototype") // must not be a singleton component as it has state
    public class JPAEntityResolver extends SimpleObjectIdResolver {
    //This would be JPA based object repository or you can EntityManager instance directly.
    private PersistentObjectRepository objectRepository;
    
    @Autowired
    public JPAEntityResolver (PersistentObjectRepository objectRepository) {
        this.objectRepository = objectRepository;
    }
    
    @Override
    public void bindItem(IdKey id, Object pojo) {
        super.bindItem(id, pojo);
    }
    
    @Override
    public Object resolveId(IdKey id) {
        Object resolved = super.resolveId(id);
        if (resolved == null) {
            resolved = _tryToLoadFromSource(id);
            bindItem(id, resolved);
        }
    
        return resolved;
    }
    
    private Object _tryToLoadFromSource(IdKey idKey) {
        requireNonNull(idKey.scope, "global scope does not supported");
    
        String id = (String) idKey.key;
        Class<?> poType = idKey.scope;
    
        return objectRepository.getById(id, poType);
    }
    
    @Override
    public ObjectIdResolver newForDeserialization(Object context) {
        return new JPAEntityResolver(objectRepository);
    }
    
    @Override
    public boolean canUseFor(ObjectIdResolver resolverType) {
        return resolverType.getClass() == JPAEntityResolver.class;
    }
    

    }

  • Скажите Jackson использовать собственный идентификатор распознавателя для класса, используя аннотацию JsonIdentityInfo ( resolver= JPAEntityResolver.class). Например,

    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
                  property = "id",
                  scope = User.class,
                  resolver = JPAObjectIdResolver.class)
    public class User { ... }
    
  • JPAObjectIdResolver - это настраиваемая реализация и будет зависеть от других ресурсов (JPA Entity Manager), которые, возможно, не будут известны Jackson. Поэтому Джексону нужна помощь в создании объекта распознавателя. Для этого вам нужно предоставить пользовательский HandlerInstantiator в ObjectMapper экземпляр. (В моем случае я использовал spring, поэтому я попросил spring создать экземпляр JPAObjectIdResolver с помощью autowiring)

  • Теперь де-сериализация должна работать как ожидалось.

Надеюсь, что это поможет.

Ответ 3

Я изменил json файл на:

[
  {"_class" : "com.example.domains.User",
   "id": 1,
   "username": "Admin",
   "password": "123Admin123",
   "activated":true
  },
  {
    "_class" : "com.example.domains.Roles",
    "id": 1,
    "user":{"_class" : "com.example.domains.User",
            "id": 1,
            "username": "Admin",
            "password": "123Admin123",
            "activated":true
          },
    "role": "Admin"
  }
]

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

Ответ 4

Если ваш bean не строго придерживается формата JavaBeans, у Джексона есть трудности.

Лучше всего создать явный конструктор @JsonCreator для вашей модели JSON bean, например.

class User {
    ...

   @JsonCreator
   public User(@JsonProperty("name") String name, 
               @JsonProperty("age") int age) {
            this.name = name;
            this.age = age;
   }

   ..
}

Ответ 5

1-1 сопоставление полей работает хорошо, но когда дело доходит до сложного сопоставления объектов, лучше использовать некоторый API. Для сопоставления экземпляров объектов можно использовать "Картографирование дозаторов" или "Mapstruct". У Dozer также есть интеграция spring.

Ответ 6

Вы можете указать нестандартные конструкторы, а затем использовать собственный десериализатор.

Что-то вроде этого должно работать (оно не было протестировано).

public class RolesDeserializer extends StdDeserializer<Roles> { 

    public RolesDeserializer() { 
        this(null); 
    } 

    public RolesDeserializer(Class<?> c) { 
        super(c); 
    }

    @Override
    public Roles deserialize(JsonParser jp, DeserializationContext dsctxt) 
      throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        long id = ((LongNode) node.get("id")).longValue();
        String roleName = node.get("role").asText();

        long userId = ((LongNode) node.get("user")).longValue();

        //Based on the userId you need to search the user and build the user object properly
        User user = new User(userId, ....);

        return new Roles(id, roleName, user);
    }
}

Затем вам нужно зарегистрировать новый десериализатор (1) или использовать аннотацию @JsonDeserialize (2)

(1)

ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Item.class, new RolesDeserializer());
mapper.registerModule(module);

Roles deserializedRol = mapper.readValue(yourjson, Roles.class);

(2)

@JsonDeserialize(using = RolesDeserializer.class)
@Entity
@Data
public class Roles {
    ...
}

Roles deserializedRol = new ObjectMapper().readValue(yourjson, Roles.class);