Когда Grails проверяет наличие устаревших объектов?

Я использую Grails 2.5.1, и у меня есть контроллер, вызывающий метод службы, который иногда приводит к ошибке StaleObjectStateException. Код в сервисном методе имеет try catch вокруг вызова obj.save(), который просто игнорирует исключение. Однако всякий раз, когда возникает один из этих конфликтов, в журнале по-прежнему печатается ошибка, и ошибка возвращается клиенту.

Мой код GameController:

def finish(String gameId) {
   def model = [:]
   Game game = gameService.findById(gameId)

   // some other work

   // this line is where the exception points to - NOT a line in GameService:
   model.game = GameSummaryView.fromGame(gameService.scoreGame(game))

   withFormat {
     json {
        render(model as JSON)
     }
   }
}

Мой код GameService:

Game scoreGame(Game game) {
   game.rounds.each { Round round ->
      // some other work
         try {
             scoreRound(round)
             if (round.save()) {
                 updated = true
             }
         } catch (StaleObjectStateException ignore) {
             // ignore and retry
         }
   }
}

Трассировка стека говорит, что исключение генерируется из моего метода GameController.finish, оно не указывает ни на какой код в моем методе GameService.scoreGame. Это означает, что Grails проверяет устаревание при запуске транзакции, а НЕ при попытке сохранения/обновления объекта?

Я сталкивался с этим исключением много раз, и обычно я исправляю его, не просматривая граф объектов.

Например, в этом случае я бы удалил ссылку game.rounds и заменил ее на:

def rounds = Round.findAllByGameId(game.id)
rounds.each {
   // ....
}

Но это означало бы, что устаревание не проверяется при создании транзакции, и это не всегда практично и, на мой взгляд, противоречит цели ленивых коллекций Grails. Если бы я хотел сам управлять всеми ассоциациями, я бы так и сделал.

Я прочитал документацию по пессимистичной и оптимистичной блокировке, но мой код следует приведенным там примерам.

Я хотел бы узнать больше о том, как/когда Grails (GORM) проверяет наличие устаревания и что с этим делать?


person Townsfolk    schedule 20.09.2015    source источник


Ответы (1)


Вы не показываете и не обсуждаете какую-либо конфигурацию транзакций, но, вероятно, это и вызывает путаницу. Основываясь на том, что вы видите, я предполагаю, что в вашем контроллере есть аннотации @Transactional. Я говорю это потому, что в этом случае транзакция начинается там, и (при условии, что ваш сервис является транзакционным) метод сервиса присоединяется к текущей транзакции.

В службе вы вызываете save(), но не очищаете сеанс. Это лучше для производительности, особенно если есть другая часть рабочего процесса, в которой вы вносите другие изменения — вам не захочется отправлять два или более набора обновлений для каждого объекта, когда вы можете отправить все изменения сразу. Поскольку вы не сбрасываете и поскольку транзакция не фиксируется в конце метода, как если бы контроллер не начал транзакцию, обновления отправляются только после завершения метода контроллера и фиксации транзакции.

Вам лучше перенести всю свою транзакционную (и бизнес) логику в сервис и удалить каждый след транзакций из ваших контроллеров. Избегайте «исправления» этого, нетерпеливо очищая, если вы не готовы принять удар по производительности.

Что касается проверки на устаревание - это достаточно просто. Когда Hibernate генерирует SQL для внесения изменений, он имеет форму UPDATE tablename SET col1=?, col2=?, ..., colN=? where id=? and version=?. Идентификатор, очевидно, будет совпадать, но если версия увеличилась, то часть версии предложения where не будет совпадать, а количество обновлений JDBC будет равно 0, а не 1, и это интерпретируется как означающее, что кто-то еще внес изменение между ваше чтение и обновление данных.

person Burt Beckwith    schedule 21.09.2015
comment
Привет, Берт, большое спасибо за быстрый ответ. На самом деле я не использую @Transactional в своем Controller, только в своем Service. Я также использую @GrailsCompileStatic в своих сервисах, не уверен, что это повлияет. У меня есть некоторая бизнес-логика в моих контроллерах, но обычно это логика, связанная с просмотром, и я только обновляю/изменяю объекты из своих служб. Существуют ли другие конфигурации транзакций, о которых мне следует знать? Как и сейчас, для Hibernate отключено все кеширование - запрос и 2-й уровень. - person Townsfolk; 21.09.2015
comment
Хорошо, значит, это не совсем так, как я описал, но похоже. Поскольку вы не сбрасываете код метода, это происходит только тогда, когда tx фиксируется - диспетчер tx поддерживает Hibernate и сбрасывает сеанс перед фиксацией. Так это происходит после вызова метода, и внутри не было бы исключения, и оно видно только в контроллере. Таким образом, вы должны использовать явную блокировку, если вы ожидаете одновременных обновлений (это немного сложно при использовании коллекций), и явную очистку, если вы хотите обнаружить проблему в сервисном методе. - person Burt Beckwith; 21.09.2015
comment
Что касается того, когда выполняется проверка устаревания - выполняется ли проверка версии также при ленивом извлечении ассоциаций? Что касается ошибки, указывающей только на строку в моем нетранзакционном контроллере, то является ли это ошибкой в ​​​​генерации трассировки стека? Я ожидаю, что верхняя часть трассировки стека, напечатанная в журналах, будет указывать на строку в GameService, где сохраняется раунд. - person Townsfolk; 21.09.2015
comment
Вызов save() для обновления постоянного экземпляра (или delete()) — это скорее сообщение для Hibernate. Hibernate пытается избежать отправки в базу данных до тех пор, пока это не потребуется, поэтому он кэширует эти изменения в сеансе до тех пор, пока сеанс не будет очищен. Таким образом, вызов save() не будет триггером - это сброс, который подталкивает изменения и вызывает проверки ограничений базы данных и проверку устаревания. Но эта проверка на устаревание выполняется только при обновлении, а не при извлечении. - person Burt Beckwith; 21.09.2015
comment
А, теперь я вижу - он выбрасывается во время фиксации транзакции, что технически происходит после вызова моего метода службы, поэтому появляется ошибка, генерируемая моим контроллером. Спасибо, что объяснили это. Я думаю, что в моем случае лучше всего подойдет явная блокировка с некоторой логикой повторных попыток. Еще раз спасибо! - person Townsfolk; 21.09.2015