Подтвердить что ты не робот

Spring @Transactional (Propagation.NEVER) должен создать сеанс спящего режима?

Предположим, что мы правильно настроили jpa, поддерживаемый hibernate (4.3.11) в spring (4.2.7). Включен кеш первого уровня Hibernate. Мы используем декларативные транзакции. У нас есть OuterBean

@Service
public class OuterBean {

    @Resource
    private UserDao userDao;

    @Resource
    private InnerBean innerBean;

    @Transactional(propagation = Propagation.NEVER)
    public void withoutTransaction(){
        User user = userDao.load(1l);
        System.out.println(user.getName());//return userName
        innerBean.withTransaction();
        user = userDao.load(1l);
        System.out.println(user.getName());//return userName instead of newUserName
    }

}

И InnerBean, вызываемый из OuterBean:

@Service
public class InnerBean {

    @Resource
    private UserDao userDao;

    @Transactional
    public void withTransaction(){
        User user = userDao.load(1l);
        user.setName("newUserName");
    }

}

Правильно ли поведение метода user.getName() в OuterBean возвращает одно и то же значение дважды (второй раз после имени обновления в базе данных)?

Другими словами, это правильное поведение, при котором @Transactional(propagation = Propagation.NEVER) создает сеанс hibernate для метода withoutTransaction(), что приводит к тому, что второй вызов user.getName() считывает из спящего кэша первого уровня вместо базы данных?

ИЗМЕНИТЬ

Чтобы объяснить проблему больше, я трассирую аттестат из создания сеансов спящего режима

TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl  - Opening Hibernate Session.  tenant=null, [email protected]
TRACE org.hibernate.internal.SessionImpl  - Opened session at timestamp: 14689173439
TRACE org.hibernate.internal.SessionImpl  - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
userName
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl  - Opening Hibernate Session.  tenant=null, [email protected]
TRACE org.hibernate.internal.SessionImpl  - Opened session at timestamp: 14689173439
TRACE org.hibernate.internal.SessionImpl  - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Automatically flushing session
TRACE org.hibernate.internal.SessionImpl  - before transaction completion
TRACE org.hibernate.internal.SessionImpl  - after transaction completion
TRACE org.hibernate.internal.SessionImpl  - Closing session
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
userName
TRACE org.hibernate.internal.SessionImpl  - Closing session

Теперь сравним трассировку при удалении @Transactional(propagation = Propagation.NEVER)

TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl  - Opening Hibernate Session.  tenant=null, [email protected]
TRACE org.hibernate.internal.SessionImpl  - Opened session at timestamp: 14689203905
TRACE org.hibernate.internal.SessionImpl  - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Closing session
userName
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl  - Opening Hibernate Session.  tenant=null, [email protected]
TRACE org.hibernate.internal.SessionImpl  - Opened session at timestamp: 14689203905
TRACE org.hibernate.internal.SessionImpl  - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Automatically flushing session
TRACE org.hibernate.internal.SessionImpl  - before transaction completion
TRACE org.hibernate.internal.SessionImpl  - after transaction completion
TRACE org.hibernate.internal.SessionImpl  - Closing session
TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl  - Opening Hibernate Session.  tenant=null, [email protected]
TRACE org.hibernate.internal.SessionImpl  - Opened session at timestamp: 14689203906
TRACE org.hibernate.internal.SessionImpl  - Setting flush mode to: AUTO
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Setting cache mode to: NORMAL
TRACE org.hibernate.internal.SessionImpl  - Closing session
newUserName

Обратите внимание, что когда я опускаю @Transactional(propagation = Propagation.NEVER), отдельный сеанс является crete для каждого вызова метода из userDao.

Поэтому мой вопрос можно сформулировать также как Должен ли @Transactional(propagation = Propagation.NEVER) реализован в spring в качестве опекуна, который мешает нам случайно использовать транзакцию без какого-либо побочного эффекта (создание сеанса)?

4b9b3361

Ответ 1

Поведение правильное - Hibernate всегда будет создавать сеанс (как еще вы ожидаете, что он выполнит какую-либо операцию?), и загрузив объект, связанный с этим сеансом. Поскольку withoutTransaction не участвует в транзакции, изменения, сделанные в пределах withTransaction, происходят в новой транзакции и не должны быть видимыми, если вы не вызываете refresh, что приведет к повторной загрузке из базы данных.

Я цитирую Официальная документация Hibernate:

Основная функция сеанса - предлагать операции создания, чтения и удаления экземпляров сопоставленных сущностей. Экземпляры могут существовать в одном из трех состояний:

  • переходный: никогда не постоянный, не связанный с какой-либо сессией
  • persistent: связанный с уникальным сеансом: ранее
  • постоянный, не связанный с какой-либо сессией

Временные экземпляры могут быть сделаны постоянными, вызывая save(), persist() или saveOrUpdate(). Стойкие экземпляры могут быть временными, вызывая delete(). Любой экземпляр, возвращаемый методом get() или load(), является постоянным.

Взято из Java Persistence With Hibernate, Second Edition от Christian Bauer, Gavin King и Gary Gregory:

Контекст persistence действует как кеш первого уровня; он запоминает все экземпляры объектов, обработанные вами в определенной единице работы. Например, если вы попросите Hibernate загрузить экземпляр объекта с использованием значения первичного ключа (поиск по идентификатору), Hibernate может сначала проверить текущую единицу работы в контексте персистентности. Если Hibernate находит экземпляр объекта в контексте персистентности, никакого попадания в базу данных не происходит - это повторяемое чтение для приложения. Последовательные вызовы em.find(Item.class, ITEM_ID) с одинаковым контекстом персистентности получат тот же результат.

Также из Java Persistence With Hibernate, Second Edition:

Кэш контекста сохранения всегда включен - он не может быть отключен. Он обеспечивает следующее:

  • Уровень персистентности не уязвим для в случае циклических ссылок в графе объектов.
  • В конце единицы работы никогда не может быть противоречивых представлений одной и той же строки базы данных. Поставщик может безопасно записывать все изменения, внесенные в экземпляр объекта в базу данных.
  • Аналогично, изменения, сделанные в конкретном контексте персистентности, всегда сразу видны для всего другого кода, выполняемого внутри этой единицы работы и ее контекста персистентности. JPA гарантирует чтение повторяющихся экземпляров объектов.

Что касается транзакций, здесь выдержка из официальной документации Hibernate:

Определяет контракт для абстрагирования приложений из сконфигурированных базовых средств управления транзакциями. Позволяет приложению определять единицы работы, сохраняя абстракцию от реализации основной транзакции (например, JTA, JDBC).

Итак, чтобы подвести итог, withTransaction и withoutTransaction не будут совместно использовать UnitOfWork и поэтому не будут использовать кеш первого уровня, поэтому второй загрузчик возвращает исходное значение.

Что касается причин, по которым эти два метода не разделяют единицу работы, вы можете обратиться к ответу Шайлендра.

EDIT:

Вы, кажется, неправильно поняли. Необходимо всегда создавать сеанс - как работает Hibernate, период. Ваше ожидание создания сеансов равно ожиданию выполнения запроса JDBC без подключения JDBC:)

Различие между двумя примерами заключается в том, что при @Transactional(propagation = Propagation.NEVER) ваш метод перехватывается и проксируется с помощью Spring, и для запросов в withoutTransaction создается только один сеанс. Когда вы удаляете аннотацию, вы исключаете свой метод из транзакционного перехватчика Spring, чтобы новый сеанс был создан для каждой операции, связанной с БД. Повторяю еще раз, и я не могу подчеркнуть это достаточно - вы должны иметь открытый сеанс для выполнения любых запросов.

Что касается защиты - попробуйте поменять аннотации на два метода, сделав withTransaction использовать Propagation.NEVER и withoutTransaction использовать аннотацию по умолчанию @Transactional и посмотреть, что происходит (спойлер: вы получите IllegalTransactionStateException).

EDIT2:

Что касается того, почему сеанс распределяется между двумя нагрузками во внешнем bean, то, что должен делать только JpaTransactionManager, и аннотируя ваш метод с помощью @Transactional, вы уведомили Spring, что он должен используйте сконфигурированный диспетчер транзакций для переноса вашего метода. Здесь официальная документация говорит о ожидаемом поведении JpaTransactionManager:

Реализация PlatformTransactionManager для одного JPA EntityManagerFactory. Привязывает JPA EntityManager из указанного factory к потоку, потенциально позволяя привязать к потоку EntityManager за factory. SharedEntityManagerCreator и @PersistenceContext знают об управляемых сущностями, связанных с потоками, и автоматически участвуют в таких транзакциях. Использование либо требуется для кода доступа JPA, поддерживающего этот механизм управления транзакциями.

Кроме того, чтобы знать, как Spring обрабатывает декларативное управление транзакциями (т.е. @Transactional аннотации к методам), обратитесь к официальной документации ., Для удобства навигации я приведу цитату:

Наиболее важными понятиями для поддержки декларативной транзакции Spring Frameworks являются то, что эта поддержка включена через прокси AOP и что транзакционные советы управляются метаданными (в настоящее время основаны на XML или аннотациях). Комбинация АОП с метаданными транзакций дает прокси-сервер AOP, который использует TransactionInterceptor в сочетании с соответствующей реализацией PlatformTransactionManager для управления транзакциями вокруг вызовов методов.

Ответ 2

@Transactional (распространение = Propagation.NEVER) все равно будет создавать сеанс. Если вы используете комбинацию Spring/Hibernate/JPA для нераспределенных транзакций, то вы, безусловно, используете JpaTransactionManager как Spring менеджер транзакций. Ответ на ваш вопрос лежит в этом классе. Хорошей идеей будет использование отладчика в вашей среде IDE, чтобы следить за тем, что происходит. Метод doBegin этого класса (который называется инфраструктурой транзакций Spring: -

protected void doBegin(Object transaction, TransactionDefinition definition) {
        JpaTransactionObject txObject = (JpaTransactionObject) transaction;

        if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            throw new IllegalTransactionStateException(
                    "Pre-bound JDBC Connection found! JpaTransactionManager does not support " +
                    "running within DataSourceTransactionManager if told to manage the DataSource itself. " +
                    "It is recommended to use a single JpaTransactionManager for all transactions " +
                    "on a single DataSource, no matter whether JPA or JDBC access.");
        }

        try {
            if (txObject.getEntityManagerHolder() == null ||
                    txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) {
                EntityManager newEm = createEntityManagerForTransaction();
                if (logger.isDebugEnabled()) {
                    logger.debug("Opened new EntityManager [" + newEm + "] for JPA transaction");
                }
                txObject.setEntityManagerHolder(new EntityManagerHolder(newEm), true);
            }

            EntityManager em = txObject.getEntityManagerHolder().getEntityManager();

            // Delegate to JpaDialect for actual transaction begin.
            final int timeoutToUse = determineTimeout(definition);
            Object transactionData = getJpaDialect().beginTransaction(em,
                    new DelegatingTransactionDefinition(definition) {
                        @Override
                        public int getTimeout() {
                            return timeoutToUse;
                        }
                    });
            txObject.setTransactionData(transactionData);

            // Register transaction timeout.
            if (timeoutToUse != TransactionDefinition.TIMEOUT_DEFAULT) {
                txObject.getEntityManagerHolder().setTimeoutInSeconds(timeoutToUse);
            }

            // Register the JPA EntityManager JDBC Connection for the DataSource, if set.
            if (getDataSource() != null) {
                ConnectionHandle conHandle = getJpaDialect().getJdbcConnection(em, definition.isReadOnly());
                if (conHandle != null) {
                    ConnectionHolder conHolder = new ConnectionHolder(conHandle);
                    if (timeoutToUse != TransactionDefinition.TIMEOUT_DEFAULT) {
                        conHolder.setTimeoutInSeconds(timeoutToUse);
                    }
                    if (logger.isDebugEnabled()) {
                        logger.debug("Exposing JPA transaction as JDBC transaction [" +
                                conHolder.getConnectionHandle() + "]");
                    }
                    TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
                    txObject.setConnectionHolder(conHolder);
                }
                else {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Not exposing JPA transaction [" + em + "] as JDBC transaction because " +
                                "JpaDialect [" + getJpaDialect() + "] does not support JDBC Connection retrieval");
                    }
                }
            }

            // Bind the entity manager holder to the thread.
            if (txObject.isNewEntityManagerHolder()) {
                TransactionSynchronizationManager.bindResource(
                        getEntityManagerFactory(), txObject.getEntityManagerHolder());
            }
            txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true);
        }

        catch (TransactionException ex) {
            closeEntityManagerAfterFailedBegin(txObject);
            throw ex;
        }
        catch (Throwable ex) {
            closeEntityManagerAfterFailedBegin(txObject);
            throw new CannotCreateTransactionException("Could not open JPA EntityManager for transaction", ex);
        }
    }

Транзакционный ресурс при использовании JPA фактически является менеджером сущности (базовая реализация - это сеанс в спящем режиме), как вы можете видеть, и это первое, что делает этот метод

EntityManager em = txObject.getEntityManagerHolder().getEntityManager();

Так определенно создается менеджер/сеанс сущности. Атрибуты транзакции затем передаются в базовый JpaDialect (HibernateJpaDialect) через TransactionDefinition. Этот класс, в свою очередь, фактически получает базовый сеанс Hibernate и API транзакций сеанса.

HibernateJpaDialect {
........
public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition)
Session session = getSession(entityManager);
entityManager.getTransaction().begin();
......
......
}
......

Ответ 3

Прежде всего, поскольку вы используете hibernate за JPA API, я буду использовать термин EntityManager вместо сеанса (строго одно и то же, только вопрос терминологии).

Каждый доступ к базе данных с использованием JPA будет включать EntityManager, вы выбираете объекты, вам нужен EntityManager (EM). То, что называется кешем первого уровня, является не более чем состоянием управляемых объектов EM.

Теоретически жизненный цикл ЭМ короткий и привязан к единице работы (и, как правило, к транзакции, см. Борьба, чтобы понять правильность использования EntityManager).

Теперь JPA может использоваться по-разному: постоянное управление, управляемое контейнером или управляемое пользователем. Когда EM управляется контейнером (ваш случай, здесь spring - это контейнер), это последнее отвечает за управление областью EM/жизненным циклом (создайте, очистите и уничтожьте его для вас). Поскольку EM ограничена транзакцией/единицей работы, эта задача делегируется TransactionManager (объект, обрабатывающий аннотации @Transactional).

Когда вы комментируете метод с помощью @Transactional(propagation = Propagation.NEVER), вы создаете область логической транзакции spring, которая гарантирует, что нет существующей базовой транзакции JDBC, связанной с возможной существующей EM, которая не создаст ее и будет использовать JDBC autocommit mode , но, который создаст EM для этой области логической транзакции, если он уже не существует.

Относительно того факта, что для каждого вызова DAO создается новый экземпляр EM, когда не определена логическая область транзакции, вы должны помнить, что вы не можете получить доступ к базе данных, используя JPA за пределами EM. Спящий режим AFAIK использовался для сброса ошибки no session bound to thread в этом случае, но это могло произойти с более поздними версиями, в противном случае ваш DAO может быть аннотирован с помощью @Transactional(propagation = Propagation.SUPPORT), который также автоматически создаст EM, если не существует замкнутой логической области. Это плохая практика, так как транзакция должна быть определена в единице работы, например. уровень обслуживания, а не DAO.

Ответ 4

Я не думаю, что это правильное поведение. Это правда, что коллеги говорят, что даже без транзакции спящий режим создает сеанс. Но это означает, что мы сталкиваемся с двумя сеансами S1 и S2 для двух отдельных чтений из DAO. В то же время кеш-память L1 всегда используется для каждого сеанса, поэтому для двух отдельных сеансов не имеет смысла использовать хит для кеша L1. Кажется, что ваш Spring не соблюдает @Transactional (распространение = распространение .NEVER)

@Transactional (propagation = Propagation.NEVER) должен быть эквивалентен, если вы просто инициализируете свою службу из основного метода и выполняете последующие вызовы в DAO самостоятельно.

Попробуйте в основном классе и посмотрите, как он будет реагировать. Я сомневаюсь, что он снова попадет в кеш L1.

Также я скопирую документ из Sprint при распространении НИКОГДА:

НИКОГДА Выполнять не транзакцию, вызывать исключение, если транзакция существует.

Еще один вопрос: настроен ли спящий режим на AutoCommit. Возможно ли, что метод "runInTransaction" не совершает?