2016-08-19 2 views
8

В принципе, я пытаюсь понять, как написать правильный (или «правильно написать»?) Транзакционный код при разработке службы REST с Jax-RS и Spring , Кроме того, мы используем JOOQ для доступа к данным. Но это не должно быть очень уместным ...
Рассмотрим простую модель, в которой у нас есть некоторые организации, у которых есть эти поля: "id", "name", "code". Все это должно быть уникальным. Также есть поле status.
Организация может быть удалена в какой-то момент. Но мы не хотим вообще удалять данные, потому что мы хотим сохранить их для целей аналитики/обслуживания. Поэтому мы просто установили поле статуса организации на 'REMOVED'.
Поскольку мы не удаляем строку организации из таблицы, мы не можем просто поместить уникальное ограничение в столбец «name», потому что мы можем удалить организацию, а затем создать новую с тем же именем. Но предположим, что коды должны быть уникальными во всем мире, поэтому у нас есть уникальное ограничение на столбец code.Как написать правильный/надежный транзакционный код с JAX-RS и Spring

Итак, давайте посмотрим на этот простой пример, который создает организацию, выполняющую некоторые проверки на этом пути.

Ресурс:

@Component 
@Path("/api/organizations/{organizationId: [0-9]+}") 
@Consumes(MediaType.APPLICATION_JSON) 
@Produces(MediaTypeEx.APPLICATION_JSON_UTF_8) 
public class OrganizationResource { 
    @Autowired 
    private OrganizationService organizationService; 

    @Autowired 
    private DtoConverter dtoConverter; 

    @POST 
    public OrganizationResponse createOrganization(@Auth Person person, CreateOrganizationRequest request) { 

     if (organizationService.checkOrganizationWithNameExists(request.name())) { 
      // this throws special Exception which is intercepted and translated to response with 409 status code 
      throw Responses.abortConflict("organization.nameExist", ImmutableMap.of("name", request.name())); 
     } 

     if (organizationService.checkOrganizationWithCodeExists(request.code())) { 
      throw Responses.abortConflict("organization.codeExists", ImmutableMap.of("code", request.code())); 
     } 

     long organizationId = organizationService.create(person.user().id(), request.name(), request.code()); 
     return dtoConverter.from(organization.findById(organizationId)); 
    } 
} 

DAO служба выглядит следующим образом:

@Transactional(DBConstants.SOME_TRANSACTION_MANAGER) 
public class OrganizationServiceImpl implements OrganizationService { 
    @Autowired 
    @Qualifier(DBConstants.SOME_DSL) 
    protected DSLContext context; 

    @Override 
    public long create(long userId, String name, String code) { 
     Organization organization = new Organization(null, userId, name, code, OrganizationStatus.ACTIVE); 
     OrganizationRecord organizationRecord = JooqUtil.insert(context, organization, ORGANIZATION); 
     return organizationRecord.getId(); 
    } 

    @Override 
    public boolean checkOrganizationWithNameExists(String name) { 
     return checkOrganizationExists(Tables.ORGANIZATION.NAME, name); 
    } 

    @Override 
    public boolean checkOrganizationWithCodeExists(String code) { 
     return checkOrganizationExists(Tables.ORGANIZATION.CODE, code); 
    } 

    private boolean checkOrganizationExists(TableField<OrganizationRecord, String> checkField, String checkValue) { 
     return context.selectCount() 
       .from(Tables.ORGANIZATION) 
       .where(checkField.eq(checkValue)) 
       .and(Tables.ORGANIZATION.ORGANIZATION_STATUS.ne(OrganizationStatus.REMOVED)) 
       .fetchOne(DSL.count()) > 0; 
    } 
} 

Это приносит некоторые вопросы:

  1. Нужен ли @Transactional аннотации createOrganization метода ресурса? Или я должен создать еще одну службу, которая ведет переговоры с DAO и добавит аннотацию @Transactional к ее методу? Что-то другое?
  2. Что произойдет, если два пользователя одновременно отправят запрос с тем же полем "code". Перед первой транзакцией транзакции успешно пройдены, поэтому не будет отправлено 409 запросов. Затем первая транзакция будет выполнена правильно, но вторая будет нарушать ограничение DB. Это вызовет SQLException. Как изящно справиться с этим? Я имею в виду, что я все еще хочу показать хорошее сообщение об ошибке на стороне клиента, заявив, что это имя уже используется. Но я не могу разобрать SQLException или что-то еще. Могу ли я?
  3. Как и предыдущий, но на этот раз «имя» не является уникальным. В этом случае вторая транзакция не будет нарушать каких-либо ограничений, что приводит к наличию двух организаций с тем же именем, что нарушает наши ограничения на доступность.
  4. Где я могу просмотреть/изучить учебники/код/​​и т. Д., Что вы считаете отличные примеры того, как писать правильный/надежный код REST + DB со сложной логикой ведения. Github/книги/блоги, что угодно. Я попытался найти что-то вроде этого myselft, но большинство примеров просто сосредоточены на сантехнике - добавьте эти библиотеки в maven, используйте эти аннотации, есть ваш простой CRUD, конец. Они вообще не содержат никаких транзакционных соображений. То есть

UPDATE: Я знаю об уровне изоляции и обычный error/isolation matrix (грязное чтение, и т.д ..). Проблема, которую я имею, заключается в нахождении какой-то «готовой к производству» выборки. Или хорошая книга по теме. Я до сих пор не понимаю, как правильно обрабатывать все ошибки. Думаю, мне нужно повторить пару раз, если транзакция завершилась неудачно .. и просто выбросить некоторую общую ошибку и реализовать клиент, который обрабатывает это. Но сделайте Мне действительно нужно использовать режим SERIALIZABLE, когда я использую запросы диапазона? Потому что это сильно повлияет на производительность. Но в остальном, как я могу гарантировать, что транзакция не удастся.

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

ответ

-1

Во первых с DAO слой не должен даже знать, что это время выходивший REST webservice. Обязательно разделяйте обязанности.

Держите @Transactional в DAO. Если вы публикуете только один отчет, вам нужно решить, согласны ли вы с грязными чтениями. В принципе, выясните, какой самый низкий уровень изоляции для вашего приложения. Каждый метод запускает новую транзакцию (если не вызван из другого метода, который уже был запущен), и, если какие-либо исключения будут сброшены, он откатится от любых вызовов. Вы можете настроить пользовательский ExceptionHandler в своем контроллере для обработки SQLDataIntegrityExceptions (например, вы вставляете пример кода).

Используйте Совокупные первичный ключ, который охватывает (идентификатор, имя, код, статус), так что вы можете иметь орг с тем же именем, но один будет «ТОК» и один будет «УДАЛЕНО»

+0

Ну, что, если есть другие действительные статусы SUSPENDED, и т.д.? Дело в том, что я не могу сделать это с помощью простых ограничений ... Должен ли я писать сложные триггеры для каждого сложного ограничения, поэтому дублируя то, что я написал на Java? Также как изящно оправиться от нарушения этих ограничений? И где я могу найти хороший пример кода, который следует этим методам? –

+0

Сфера действия транзакции является единицей работы, единица работ не определена на уровне DAO, вообще говоря, наличие @Transactionnal аннотаций на уровне DAO является конструктивным запахом – Gab

+0

@Gab Очень не согласен - где, по вашему мнению, информация о транзакциях должна быть если не так близко к тому, где взаимодействуют базы данных? – Gandalf

1

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

Кажется, что ваши методы checkXXX являются частью бизнес-логики, потому что они вызывают ошибки в конфликтах, специфичных для доменов. Почему бы не включить их в Сервис в один метод, который, кстати, является транзакционным?

//service code 
public Organization createOrganization(String userId, String name, String code) { 

    if (this.checkOrganizationWithNameExists(request.name())) { 
     throw ... 
    } 

    if (this.checkOrganizationWithCodeExists(code)) { 
     throw ... 
    } 

    long organizationId = this.create(userId, name, code); 
    return dao.findById(organizationId); 
} 

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

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

//endpoint code 
@POST 
public OrganizationResponse createOrganization(@Auth Person person, CreateOrganizationRequest request) { 
    String code = request.code(); 
    String name = request.name(); 
    String userId = person.user().id(); 
    return dtoConverter.from(organizationService.createOrganization(userId, name, code)); 
} 

Что касается вопроса 2 и 3, transaction isolation levels ваши друзья. Положите уровень изоляции достаточно высоким. Я думаю, что «повторяемое чтение» является подходящим в вашем случае. В ваших методах checkXXX будет обнаружено, что какая-либо другая транзакция совершает сущности с одинаковым именем или кодом, и это гарантирует, что ситуации остаются на момент создания метода. Еще один useful read относительно уровней изоляции Spring и транзакций.

+0

Но это все еще не помогает решить проблему с неправильными данными. Смысл вопросов 2 и 3 в вопросе ... –

+0

Проверка данных выполняется в сервисе, и вы бросаете свое собственное исключение. Вам не нужно разбирать SQLException. Изоляция транзакций гарантирует, что после выполнения проверок в службе они все еще действительны во время операции «Создать». Даже если два запроса поступят в одно и то же время, один из них будет задержан до тех пор, пока другой не будет совершен (или что-то в этом роде, это зависит от уровня). Я думаю, что это касается вопросов 2 и 3. – pcjuzer

+0

Это не так просто ... SERIALIZABLE изоляция слишком ограничительна для использования в каждом случае, но даже REPEATABLE_READ недостаточно. Я должен был использовать SELECT FOR UPDATE, чтобы заставить его работать как-то, но таким образом будет происходить параллелизм ... BTW, я поставил тестовый проект на github, чтобы каждый мог поиграть с ним ... https://github.com/ Fantastic/gs-management-transactions –

1

Как я понимаю, лучший способ справиться с транзакцией уровня DB должен эффективно использовать изоляцию изоляции Spring на уровне dao. Ниже приведен пример промышленного стандарта Codde в вашем случае ...

public interface OrganizationService { 
    @Retryable(maxAttempts=3,value=DataAccessResourceFailureException.class,[email protected](delay = 1000)) 
    public boolean checkOrganizationWithNameExists(String name);  
} 

@Repository 
@EnableRetry 
public class OrganizationServiceImpl implements OrganizationService { 
    @Transactional(isolation = Isolation.READ_COMMITTED) 
    @Override 
    public boolean checkOrganizationWithNameExists(String name){ 
     //your code 
     return true;  
    } 
} 

Пожалуйста ущипнуть меня, если я ошибаюсь здесь

0

Разделение относятся:

  • JAX-RS ресурсов (конечная точка): просто обработайте запрос, вызовите службу и оберните потенциальное исключение в соответствующий код ответа (просто поймите и оберните вручную или используйте exception mapper).
  • Уровень обслуживания/бизнес: вывести транзакционный метод для каждой единицы работы, деловая ошибка должна обрабатываться как проверенное исключение, а операционные - как неконтролируемые (подклассы RuntimeException).
  • Уровень доступа к данным: просто обработайте материал доступа к данным (т.get db context, выполняет запрос и в конечном итоге отображает результат).

Я настаиваю на одном: хорошее место для границ транзакций - это место, где определяются ваши бизнес-методы. Объем транзакции должен быть бизнес-единицей работы.

Что касается проблемы параллелизма, существует два способа решения этой проблемы параллелизма: пессимистическая или оптимистичная блокировка.

  • Пессимистический:

    • Замок
    • сделать свой материал
    • Update
    • БЛокИРовкИ
  • Оптимистичный:

    • проверка версии
    • сделать ваши вещи
    • обновления, если версия же, не в противном случае

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

Я бы лично пойти с оптимистической блокировки в вашем случае, JOOQ support it

+0

Для оптимистичной блокировки Непонятно, какие записи следует использовать для версии? Я хочу защитить операцию INSERT от создания повторяющихся записей. Но в этом случае я не меняю никаких записей, не так ли? Я думаю, я могу заставить его работать как-нибудь .. как создать таблицу, содержащую все имена, которые когда-либо использовались, но это кажется уродливым ... Что касается пессимистической блокировки - значит, я должен использовать 'SELECT..FOR UPDATE'? Или просто используйте изоляцию 'REPEATABLE READ'? –

+0

Я просто не могу понять, если он действительно защитит мое дело, где я ВСТАВЯ НОВЫЕ записи как решение, сделанное путем выбора других записей, I.e. проверьте, существует ли имя, затем вставьте новую запись с этим именем, иначе ничего не делайте. –

+0

Sry Я читал слишком быстро. 'REPEATABLE READ' не препятствует вставке новых данных, вам необходимо заблокировать всю таблицу и поэтому установить уровень на сериализуемый. Я полагаю, что 'select for update' только блокирует выбранный оператор и не подходит. Вы можете сохранить запись в виде таблицы org в отдельной таблице, чтобы реализовать оптимистичную блокировку и вставить новую версию org и increment в ту же транзакцию, но она не очень элегантная – Gab