Jdbi: Сделайте @CreateSqlObject менее запутанным.

Созданный на 1 февр. 2018  ·  20Комментарии  ·  Источник: jdbi/jdbi

Проблема
Официальная документация JDBI наводит разработчиков на мысль, что аннотация @CreateSqlObject — это механизм добавления поддержки транзакций в разрозненные DAO.

http://jdbi.org/#__createsqlobject

Это кажется немного неверным по отношению к фактическому поведению, которое может привести к ошибкам состояния при использовании с предпочтительным механизмом создания jdbi.onDemand .

На форуме было несколько сообщений, иллюстрирующих эту путаницу:

Запрос
Не уверен, что лучшее решение (я) этой проблемы было бы. Некоторые, которые приходят на ум:

  • Устаревание
  • Лучшая документация по недостаткам
  • Еще одно решение для разделения запросов на несколько DAO, позволяющее связать их вместе с помощью логической транзакционной семантики. В частности, в поддержке сочетания транзакций только для чтения/записи.
cleanup improvement

Самый полезный комментарий

Извините, что снова комментирую это. Есть ли в вашей дорожной карте что-нибудь, что поможет в этом?

Совершенно никаких проблем. Это явно источник путаницы, и мы хотели бы это исправить — мы просто действительно хотим убедиться, что на этот раз все сделано правильно, поскольку в прошлый раз мы не сделали этого :)

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

Да, но насколько я понимаю, все это делается с помощью хуков АОП в вашей структуре DI (например, Spring). Поскольку JDBI не «обертывает» все ваши сервисные объекты, у нас нет возможности предоставлять АОП-решения для объектов, которые мы не создаем сами. Даже старая реализация cglib замечала бы только @Transactional на самих даосах, она бы никогда не работала на отдельном сервисном классе.

Если это было бы полезно, мы могли бы рассмотреть возможность добавления, например, привязок Spring и/или Guice AOP, чтобы разрешить транзакции на более высоком уровне. Это будет не часть core , а, например, часть расширений spring . Вряд ли это будет чем-то, на что ухватятся основные разработчики, но вклад будет серьезно рассмотрен для включения. Может быть, это решит вашу проблему более «чистым» способом?

Мы решили перейти с cglib на Proxy в основном из соображений совместимости — Proxy — это поддерживаемый jdk API, тогда как cglib (или, в частности, asm ) имеет тенденцию ломаться с каждым крупным выпуском (он сломался в 8, 9, 11, ...), что превращается в огромную головную боль при обслуживании.

Все 20 Комментарий

Спасибо, что сообщили об этом. Я подозреваю, что мы закончим здесь технически неправильным изменением, но я думаю, что существующее поведение достаточно плохое, чтобы мы внесли технически критическое изменение в начале цикла выпуска 3.x. Пожалуйста, отметьте эту проблему, если вы зависите от существующего поведения или не согласны с возможной отправкой небольшого критического изменения здесь.

Просто для контекста, как _why_ @CreateSqlObject не очень хорошо работает с запросом:

  • On-demand — это абстракция уровня ядра, реализованная с использованием прокси. Когда вы вызываете метод на прокси-сервере по требованию, создается реальный объект SQL, и вызов метода делегируется реальному объекту SQL. Дескриптор, поддерживающий этот реальный экземпляр, закрывается после возврата вызова метода делегата.
  • @CreateSqlObject реализован с использованием handle.attach(sqlObjectType) с исходным дескриптором поддержки объекта SQL.

Таким образом, резервный дескриптор для созданного объекта SQL закрывается еще до того, как объект SQL может быть возвращен.

Ничто из вышеперечисленного не высечено на камне — это просто то, как это реализовано сейчас.

Я не уверен, что нам придется нарушать совместимость, чтобы исправить это.

Извините, я не очень понимаю... В чем проблема с onDemand и CreateSqlObject? У меня никогда не было проблем с этим, и что-то в объяснении @qualidafial просто заставляет меня задуматься...

image

Соединение устанавливается для вызова fooProxy.usecase . Foo.usecase вызывает метод bar createSqlObject, который возвращает новый Bar после присоединения его к текущему ( usecase ) дескриптору. дескриптор usecase закрывается, когда возвращается usecase . Пока вы не делаете ничего асинхронного с возвращенным Bar , как может срок действия дескриптора bar истекать слишком рано? Жизненный цикл и использование bar ограничены телом Foo.usecase , как и дескриптор...

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

В принципе, в наших более низких средах все было в порядке. Но с увеличением нагрузки в производственной среде мы постоянно видим, что операторы SQL не выполняются на соответствующем узле базы данных. Например, выбор «Только для чтения» повлияет на главный узел, а «Вставка» — на реплику чтения. Кроме того, мы получили бы ошибки закрытия соединения, когда оно было в середине операции.

По сути, казалось, что @CreateSqlObject создал проблему безопасности потоков, когда флаг только для чтения был изменен конкурирующими запросами.

Чтобы «решить» проблему, мы удалили все использование @CreateSqlObject и в итоге получили что-то вроде этого:

  • class FooDao
  • class BarDao
  • class CombinedDao extends FooDao, BarDao

А то мы jdbi.onDemand(CombinedDao) и пользуемся доступом только через него.
Переход на этот шаблон, хотя и не такой красивый, избавил от всех вышеупомянутых ошибок в продакшене.

Просто для информации, я думаю вернуться к JDBI 2, потому что весь мой код на данный момент использует onDemand с абстрактными классами, и я не могу решить, как реструктурировать так, чтобы я был доволен!

Я предпочитаю сервисные интерфейсы с абстрактными классами реализации, содержащими логику, но без SQL, с методами, аннотированными как транзакционные, с использованием CreateSqlObject для предоставления доступа к классам DAO, которые являются чисто SQL. Упрощенный пример:

Интерфейс

public interface AccountService {
    void addAccount(Account account, User user);
}  

Выполнение

public abstract class AccountServiceJdbi implements AccountService {

    <strong i="11">@Override</strong>  
    <strong i="12">@Transaction</strong>  
    public final void addAccount(@BindBean() Account account, User user) {
        long accountId =  accountDao().insertAccount(account);
        accountDao().linkAccountToOwner(accountId, user.getId());
    }

    <strong i="13">@CreateSqlObject</strong>
    abstract AccountDao accountDao();
}

(Вы можете представить Дао)

Это дает действительно хорошее разделение логики и доступа к данным, позволяя транзакции по нескольким методам DAO. Модульные тесты для службы легко написать и понять благодаря реализации интерфейсов DAO.

Я пытался сделать подобное в JDBI 3, но я думаю, что это означает, что реализация моего класса обслуживания должна быть интерфейсом с методами по умолчанию для тех, которые содержат логику. Методы по умолчанию нельзя сделать окончательными, поэтому код просто не кажется таким кратким и дает мне меньше контроля над тем, как можно использовать мой класс.

Могу ли я каким-либо образом структурировать свой код в JDBI 3, чтобы иметь окончательные методы транзакций?

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

Однако вы _можете_ определить свои собственные аннотации методов SQL наравне с @SqlQuery , @SqlUpdate и т. д. и предоставить статическую реализацию для методов с этой аннотацией.

Однако вы _можете_ определить свои собственные аннотации методов SQL наравне с @SqlQuery , @SqlUpdate и т. д. и предоставить статическую реализацию для методов с этой аннотацией.

Спасибо за ответ - я не совсем понимаю, как это мне помогает. Каков рекомендуемый способ иметь запросы в нескольких DAO, но выполнять их в одной транзакции?

способ иметь запросы в нескольких DAO, но выполнять их в одной транзакции?

Лично я использую что-то вроде

interface Dao1 {
  @SqlQuery("...")
  void query1();
}

interface Dao2 {
  @SqlQuery("...")
  void query2();
}

interface JdbiServiceImpl extends Service {
  <strong i="8">@CreateSqlObject</strong>
  Dao1 dao1();
  <strong i="9">@CreateSqlObject</strong>
  Dao2 dao2();

  <strong i="10">@Transaction</strong>
  <strong i="11">@Override</strong>
  void businessCase() {
    dao1().query1();
    dao2().query2();
  }
}

Service service = handle.attach(JdbiServiceImpl.class);
service.businessCase();

Отлично работает для меня таким образом. @CreateSqlObject в основном похож на инъекцию зависимостей Spring с помощью getter/setter. Я помещаю экземпляры onDemand в свой контекст spring как bean-компоненты, поэтому он работает точно так же, как обычные службы, вызывающим абонентам не нужно знать о jdbi или интерфейсе реализации.

image

То, что вы видите в StockReductionCase, похожее на анти-шаблон, является примером вложенного jdbi-«внедрения зависимостей» с CreateSqlObject. Внутри него есть собственные зависимости, как и в реализации службы. По сути, это отдельная служба, я просто называю ее Case вместо Service, чтобы избежать циклической зависимости, помещая только Cases (необходимые в нескольких местах) внутри Services (неповторно используемая логика верхнего уровня) и Queries в любой из них.

image

Я слышал смешанные сообщения об успешном получении транзакций для правильной области видимости с помощью @CreateSqlObject , особенно в сочетании с onDemand() .

Самый надежный способ выполнить несколько объектов SQL в одной транзакции — запустить транзакции через Handle.inTransaction() или Jdbi.inTransaction() . Внутри обратного вызова любые DAO, созданные с помощью Handle.attach() , будут частью транзакции дескриптора:

jdbi.useTransaction(handle -> {
  Dao1 dao1 = handle.attach(Dao1.class);
  Dao2 dao2 = handle.attach(Dao2.class);

  dao1.doStuff();
  dao2.doMoreStuff();
});

Спасибо за ответы
@qualidafial это похоже на то, как мне придется идти, но это затрудняет написание модульных тестов для класса обслуживания. Поскольку он создает сам Daos, я не могу заменить фиктивные Daos модульными тестами.

@TheRealMarnes тоже спасибо - это похоже на то, что я пробовал, просто мне не нравится использовать методы по умолчанию, поскольку их можно переопределить.

@qualidafial Я хотел бы подтвердить вам, что мы не можем использовать аннотацию jdbi @Transaction для методов службы, которые вызывают несколько методов dao?

Если это предположение верно, такое поведение очень опасно, если оно не задокументировано должным образом как часть официальной документации. Большое количество разработчиков имеют опыт работы со стеком Spring, и использование аннотации @Transactional является очень стандартным способом поддержки транзакций между несколькими DAO.

@svlada Аннотация @Transaction _только_ работает с методами объектов SQL. Когда вы говорите «методы обслуживания», у меня создается впечатление, что вы используете аннотацию к какому-то объекту, находящемуся вне влияния Jdbi, например, внедренному классу.

Пример из нашего набора тестов:

<strong i="9">@Test</strong>
public void testInsertAndFind() {
    Foo foo = handle.attach(Foo.class);
    Something s = foo.insertAndFind(1, "Stephane");
    assertThat(s).isEqualTo(new Something(1, "Stephane"));
}

<strong i="10">@Test</strong>
public void testTransactionPropagates() {
    Foo foo = dbRule.getJdbi().open().attach(Foo.class);

    assertThatExceptionOfType(Exception.class)
        .isThrownBy(() -> foo.insertAndFail(1, "Jeff"));

    Something n = foo.createBar().findById(1);
    assertThat(n).isNull();
}

public interface Foo {
    <strong i="11">@CreateSqlObject</strong>
    Bar createBar();

    @SqlUpdate("insert into something (id, name) values (:id, :name)")
    int insert(@Bind("id") int id, @Bind("name") String name);

    <strong i="12">@Transaction</strong>
    default Something insertAndFind(int id, String name) {
        insert(id, name);
        return createBar().findById(id);
    }

    <strong i="13">@Transaction</strong>
    default Something insertAndFail(int id, String name) {
        insert(id, name);
        return createBar().explode();
    }
}

public interface Bar {
    @SqlQuery("select id, name from something where id = :id")
    Something findById(@Bind("id") int id);

    default Something explode() {
        throw new RuntimeException();
    }
}

Я также хочу уточнить в отношении onDemand() + @CreateSqlObject :

  • @CreateSqlObject DAO можно использовать только из внутренних методов создавшего их DAO, как в приведенном выше примере.
  • Вызов fooDao.createBar().findById() вызовет исключение, указывающее, что соединение закрыто.

Извините, что снова комментирую это. Есть ли в вашей дорожной карте что-нибудь, что поможет в этом?

Я до сих пор очень скучаю по возможности использовать CreateSqlObject в абстрактных классах. Необходимость делать все в интерфейсах по-прежнему кажется ограничивающей; часто я хотел бы сделать метод типа «служба» транзакционным, но это означало бы, что мои классы службы также должны были быть интерфейсами, использующими методы по умолчанию, и я начинаю терять ясность разделения проблем, конкретных конечных методов и т. д.

@tamslinn , что касается вашей проблемы с интерфейсами вместо абстрактных классов, я думаю, что могу с уверенностью сказать, что в ближайшем будущем мы не вернемся к этому решению. Jdbi3 использует прокси-серверы jdk, которые поддерживают только интерфейсы.

Я не думаю, что отдельный модуль, построенный на cglib, или что-то подобное, предлагающий эту функцию, полностью исключен, но сейчас это не проблема.

Если аксиомы sqlobject вам так плохо подходят, вы всегда можете реорганизовать свои службы, чтобы они были обычными классами, не улучшенными jdbi, а вместо этого были внедрены экземпляры Jdbi для использования свободного API.

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

Большое спасибо за обновление. Полностью имеет смысл относительно прокси.
Конкретным ограничением для меня является невозможность запуска/завершения транзакций вне классов JDBI.
(Как уже упоминалось @svlada , в других фреймворках довольно часто используется @Transactional на более высоком уровне)
Мне очень нравится все остальное в JDBI, поэтому я продолжу, может быть, я придумаю шаблон проектирования, которым я доволен для своих классов обслуживания :)

Извините, что снова комментирую это. Есть ли в вашей дорожной карте что-нибудь, что поможет в этом?

Совершенно никаких проблем. Это явно источник путаницы, и мы хотели бы это исправить — мы просто действительно хотим убедиться, что на этот раз все сделано правильно, поскольку в прошлый раз мы не сделали этого :)

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

Да, но насколько я понимаю, все это делается с помощью хуков АОП в вашей структуре DI (например, Spring). Поскольку JDBI не «обертывает» все ваши сервисные объекты, у нас нет возможности предоставлять АОП-решения для объектов, которые мы не создаем сами. Даже старая реализация cglib замечала бы только @Transactional на самих даосах, она бы никогда не работала на отдельном сервисном классе.

Если это было бы полезно, мы могли бы рассмотреть возможность добавления, например, привязок Spring и/или Guice AOP, чтобы разрешить транзакции на более высоком уровне. Это будет не часть core , а, например, часть расширений spring . Вряд ли это будет чем-то, на что ухватятся основные разработчики, но вклад будет серьезно рассмотрен для включения. Может быть, это решит вашу проблему более «чистым» способом?

Мы решили перейти с cglib на Proxy в основном из соображений совместимости — Proxy — это поддерживаемый jdk API, тогда как cglib (или, в частности, asm ) имеет тенденцию ломаться с каждым крупным выпуском (он сломался в 8, 9, 11, ...), что превращается в огромную головную боль при обслуживании.

Может быть, https://github.com/jdbi/jdbi/pull/1252 имеет значение?

Привет,

Спасибо за информацию @stevenschlansker. На самом деле я не использую Spring с JDBI для своего текущего проекта, я просто упомянул для сравнения. Я полностью понимаю, что такое АОП, просто пытаюсь найти способ обойти это, что означает, что я могу разделить код, как я хочу - абстрактные классы работали на меня, но имеет смысл отойти от cglib

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

         try (Handle handle = jdbi.open()) {
                handle.useTransaction(h -> {
                    AccountService accountService = new AccountServiceImpl(h.attach(AccountDao.class));                    
                    accountService.addAccount(a, u);
                });
          }

Я думаю, это будет означать, что я могу модульно тестировать службу с помощью Mock Dao, сохранять методы службы окончательными и не полагаться на методы по умолчанию в интерфейсах для управления транзакциями, а дополнительные накладные расходы на создание экземпляра класса службы не должны иметь существенного влияния.

Просто быстрый ответ на пример кода в вашем последнем комментарии: вы можете пропустить блок try и просто вызвать jdbi.useTransaction() напрямую. Этот метод выделяет временный дескриптор, который автоматически закрывается при возврате обратного вызова.

Привет всем, есть PR # 1579 - начиная с jdbi 3.10.0, CreateSqlObject и onDemand должны (наконец-то) хорошо работать вместе.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги