Проблема Spring EntityManager.persist

Я использую платформу Spring (Boot + web + jpa) для службы REST, и мне нужно сохранить объект под названием Sensor. Поскольку я использовал Hibernate, я хотел продолжать использовать EntityManager. Из документа Spring Data Access и ответов на некоторые вопросы в Stackoverflow я понял, что мне нужно настроить LocalEntityManagerFactoryBean или LocalContainerEntityManagerFactoryBean.

Теперь я смог прочитать persist.xml и сохранить сущность посредством внедрения зависимостей @PersistenceUnit(unitName = "...") EntityManagerFactory в случае с LocalContainerEntityManagerFactoryBean. Однако Spring продолжает выдавать ошибку каждый раз, когда я пытаюсь сохранить экземпляр new Sensor() с помощью LocalEntityManagerFactoryBean, как показано ниже.

// LocalEntityManagerFactoryBean
@Bean
public LocalEntityManagerFactoryBean entityManagerFactory() {
    LocalEntityManagerFactoryBean em = new LocalEntityManagerFactoryBean();
    em.setPersistenceUnitName("myunit");
    JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    em.setJpaVendorAdapter(vendorAdapter);
    return em;
}

Сохранение (transaction.begin и end находятся в вызывающем методе):

private Sensor getSensor(SimpleData sd) {
    List<Sensor> sensors = entityManager.createQuery("select s from Sensor s where s.deviceId = :devid", Sensor.class)
                                     .setParameter("devid", sd.getDeviceId())
                                     .getResultList();
    Sensor sensor = null;
    if (sensors.isEmpty()) {
        sensor = new Sensor();
        sensor.setDeviceId(sd.getDeviceId());
        sensor.setName(sd.getName());
        entityManager.persist(sensor); // Throws an exception
        deviceIdMapper.put(sensor.getDeviceId(), sensor.getId());
        logger.trace("Cannot find the sensor in db, adding: "+sensor.getDeviceId());
    } else {
        sensor = sensors.get(0);
        deviceIdMapper.put(sensor.getDeviceId(), sensor.getId());
        logger.trace("Found a sensor from the db: "+sd.getDeviceId());
    }
    deviceIdMapper.putIfAbsent(sensor.getDeviceId(), sensor.getId());
    logger.trace(sensor);
    return sensor;
}

Исключение:

javax.persistence.PersistenceException: org.hibernate.property.access.spi.PropertyAccessException: Error accessing field [protected int com.database.Sensor.id] by reflection for persistent property [com.database.Sensor#id] : com.database.Sensor@6de9600a
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:154)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:188)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:807)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:785)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:350)
at com.sun.proxy.$Proxy108.persist(Unknown Source)
at com.DataListener.getSensor(DataListener.java:69)
at com.DataListener.lambda$0(DataListener.java:87)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at com.DataListener.processData(DataListener.java:86)
at com.DataListener.onMessage(DataListener.java:49)
at org.apache.activemq.ActiveMQMessageConsumer.dispatch(ActiveMQMessageConsumer.java:1321)
at org.apache.activemq.ActiveMQSessionExecutor.dispatch(ActiveMQSessionExecutor.java:131)
at org.apache.activemq.ActiveMQSessionExecutor.iterate(ActiveMQSessionExecutor.java:202)
at org.apache.activemq.thread.PooledTaskRunner.runTask(PooledTaskRunner.java:129)
at org.apache.activemq.thread.PooledTaskRunner$1.run(PooledTaskRunner.java:47)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: org.hibernate.property.access.spi.PropertyAccessException: Error accessing field [protected int com.database.Sensor.id] by reflection for persistent property [com.database.Sensor#id] : com.database.Sensor@6de9600a
at org.hibernate.property.access.spi.GetterFieldImpl.get(GetterFieldImpl.java:75)
at org.hibernate.tuple.entity.AbstractEntityTuplizer.getIdentifier(AbstractEntityTuplizer.java:224)
at org.hibernate.persister.entity.AbstractEntityPersister.getIdentifier(AbstractEntityPersister.java:4931)
at org.hibernate.persister.entity.AbstractEntityPersister.isTransient(AbstractEntityPersister.java:4631)
at org.hibernate.engine.internal.ForeignKeys.isTransient(ForeignKeys.java:226)
at org.hibernate.event.internal.AbstractSaveEventListener.getEntityState(AbstractSaveEventListener.java:540)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:102)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:62)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:800)
... 20 more
Caused by: java.lang.IllegalArgumentException: Can not set int field com.database.Sensor.id to com.database.Sensor
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171)
at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.ensureObj(UnsafeFieldAccessorImpl.java:58)
at java.base/jdk.internal.reflect.UnsafeIntegerFieldAccessorImpl.getInt(UnsafeIntegerFieldAccessorImpl.java:56)
at java.base/java.lang.reflect.Field.getInt(Field.java:594)
at org.hibernate.property.access.spi.GetterFieldImpl.get(GetterFieldImpl.java:62)
... 28 more

Класс датчика:

@Entity
public class Sensor {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected int id;
    protected String deviceId;
    protected String name;

    @OneToMany(mappedBy = "owner", cascade=CascadeType.ALL)
    private List<SimpleData> simpleDevices = new ArrayList<>();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDeviceId() {
        return deviceId;
    }

    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }


    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public List<SimpleData> getSimpleDevices() {
        return simpleDevices;
    }

    public void setSimpleDevices(List<SimpleData> simpleDevices) {
        this.simpleDevices = simpleDevices;
    }

}

Есть два вопроса:

  1. Почему ошибка?
  2. Spring выдает ошибку, если я не предоставляю DataSource для LocalContainerEntityManagerFactoryBean? LocalEntityManagerFactoryBean использует конфигурации DataSource из persistence.xml. Почему LocalContainerEntityManagerFactoryBean не делает то же самое ?:

Редактировать 1

Думаю, я нашел решение. Если я удалю spring-boot-devtools из зависимости, это решит проблему. Фактически, когда я использую spring-boot-devtools, я не мог читать Sensor из-за какой-то проблемы с ClassLoader. Может баг какой то ?:

java.lang.ClassCastException: class com.database.Sensor cannot be cast to class com.database.Sensor (com.database.Sensor is in unnamed module of loader 'app'; com.database.Sensor is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @6228d409)

Редактировать 2 Вот класс SimpleData

@Entity
public class SimpleData {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private double value;

    @JsonIgnore
    private Date time;

    @Transient
    private String deviceId;

    @JsonIgnore
    @Transient
    private String name;

    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "sensor_id")
    private Sensor owner;

    public double getValue() {
        return value;
    }

    public void setValue(double value) {
        this.value = value;
    }

    public Date getTime() {
        return time;
    }

    public void setTime(Date time) {
        this.time = time;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDeviceId() {
        return deviceId;
    }

    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }

    public Sensor getOwner() {
        return owner;
    }

    public void setOwner(Sensor owner) {
        this.owner = owner;
    }
}

Редактировать 3

Хорошо, я сделал небольшую тестовую программу, которая воспроизводит проблему: https://github.com/hjparker/spring-boot-devtools-issue.

Как только сервер заработает, вы можете сделать GET-запрос на localhost: 8080, который выдаст ошибку. Комментирование spring-boot-devtools в build.gradle решит проблему. Я обнаружил, что это как-то связано с перезапуском загрузчика классов в spring-dev-tools (https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html), который перезагружает разрабатываемый код каждый раз при перезапуске Spring. Но я думаю, что некоторая часть файла не перезагружается автоматически, вызывая classcastexception, например. Сущность, загруженная с помощью CL1! = Сущность, загруженная с помощью CL2. Может кто-нибудь объяснить, если я что-то делаю не так?

Вы вообще используете Spring Boot? Обратите внимание, что ваше сообщение об ошибке конкретно относится к вашему классу Sensor, который вы не смогли включить.

chrylis -cautiouslyoptimistic- 25.11.2018 02:21

Извините, я отредактировал вопрос, включив класс Sensor. Да, я использую Spring Boot.

Domination 25.11.2018 02:25

Если вы используете Boot, прекратите настраивать всю систему JPA вручную и просто позвольте Boot сделать это за вас. Тем не менее, я не думаю, что проблема в вашей настройке Hibernate; похоже, это что-то с вашим классом. Попробуйте использовать Integer для своего поля идентификатора (и обратите внимание, что в большинстве случаев, если вы используете интегральные типы, Long - это правильный выбор, потому что 32 бита - это не так уж и много, особенно если у вас есть последовательности).

chrylis -cautiouslyoptimistic- 25.11.2018 02:36

Лучше использовать оболочку, чем примитивный тип, поэтому попробуйте использовать защищенный Integer id;

TinyOS 25.11.2018 02:41

Думаю, я нашел решение. Мне нужно было удалить spring-devtools из зависимостей. Но интересно, почему? Если у меня есть инструменты разработчика и я пытаюсь прочитать класс Sensor из БД, возникает следующая ошибка. java.lang.ClassCastException: класс com.database.Sensor не может быть преобразован в класс com.database.Sensor (com.database.Sensor находится в безымянном модуле загрузчика 'app'; com.database.Sensor находится в безымянном модуле загрузчика org .springframework.boot.devtools.restart.classloader.Restar‌ tClassLoader @ 6228d409)

Domination 25.11.2018 03:07

Не могли бы вы показать мне ваш SimpleData.class?

TinyOS 25.11.2018 03:48

Добавлен к тексту

Domination 25.11.2018 03:58
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
7
1 081
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Необходимо обновить модификатор доступа private вместо защищенного, похоже, что постоянство Java пытается получить идентификатор, которого нет в пакете, и не вызывается в каком-то подклассе.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String deviceId;
private String name;

Документация по модификаторам доступа

Изменил его на частный, но результат тот же. Как я уже упоминал при редактировании, проблема возникла из-за spring-boot-devtools. Теперь мне интересно, почему это вызывает проблему. Я думаю, что это как-то связано с двумя экземплярами Sensor из разных ClassLoader из-за добавления spring-boot-devtools.

Domination 25.11.2018 05:54

Все в порядке, чем я могу вам помочь? не могли бы вы поделиться своим репо?

Jonathan JOhx 25.11.2018 06:14

Я сделал небольшую программу, в которой возникает проблема. См. Обновленный вопрос.

Domination 25.11.2018 15:10
Ответ принят как подходящий

Кажется, что некоторые десериализаторы для классов Entity или Bean, управляемые Hibernate / Spring ORM, не перезагружаются Spring-boot-devtools, перезапускают загрузчик классов должным образом. Мне пришлось явно включить классы, выполняющие десериализацию, в загрузчик классов перезапуска, установив следующие свойства в META-INF/spring-devtools.properties:

restart.include.hibernate=hibernate.*
restart.include.spring-orm=spring-orm.*

Источники:

https://github.com/AxonFramework/AxonFramework/issues/344

https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html#using-boot-devtools-customizing-classload

Другие вопросы по теме