将应用程序从Grails 2.5迁移到Grails 3.1后,我遇到了一个奇怪的问题 - 数据库异常永远不会冒泡到控制器。
典型方案如下所示:控制器的删除操作尝试删除条目,但数据库级约束阻止删除。这会导致异常(见下文) - 在 Grails 2.5 中,如果未捕获,这将导致"错误 500 内部服务器错误"。现在,我们有一个自定义映射,因此即使发生未捕获的错误,也会以用户友好的方式处理,并且用户知道发生了某些事情。
在Grails 3中,这停止了工作。将记录异常,但永远不会冒泡到控制器。因此,在这种特定情况下,我们的控制器只是继续返回"204"HTTP状态代码,就好像什么都没发生一样,最终用户甚至不知道出了什么问题。
我很困惑,我想这一定是我错过了什么愚蠢的东西,但无法弄清楚。我已经测试了这个Grails 2.5.5,3.1.16,3.2.12和3.3.2,但所有测试的3.x版本的行为方式几乎相同。还用Hibernate 4和5进行了测试,并尝试了H2和PostgreSQL,但无论如何我都得到了相同的行为。
重现的最简单方法是手动将 FK 关系(不执行删除操作)添加到休眠生成的数据库架构,并尝试通过控制器删除(下面的示例 #1),但某些多对多关系或其他情况(如保留新条目(并在数据库级别失败))也可能出现问题。
更新#1,2018年4月25日:要使用PostgreSQL作为数据库和Grails 3.3.x创建一个最小的可重现测试用例,只需通过grails create-app testapp
创建一个空白的grails应用程序。然后调整 application.yml 以配置 PostgreSQL 实例的数据源,并使用dbCreate: validate
模式,如下所示。
添加一个最小的域对象:
class Book {
String name
}
还有一个用于测试对象删除的控制器,即 BookController:
class BookController {
@Transactional
def delete(Long id) {
Book.get(id).delete()
render "Book $id deleted"
}
}
下面的架构创建 2 个包含 FK 和几个条目的表。ID 为 1 的图书将无法移除,ID 为 2 和 3 的图书将成功移除。控制器将在所有 3 种情况下返回 200 OK。您可以通过 curl 调用使用默认的 UrlMappings 进行测试,即curl http://localhost:8080/book/delete/1
.
数据库架构如下:
CREATE TABLE book (
id bigint NOT NULL,
version bigint NOT NULL,
name character varying(255) NOT NULL,
CONSTRAINT book_pkey PRIMARY KEY (id)
);
CREATE TABLE chapter (
id bigint NOT NULL,
book_id bigint NOT NULL,
name character varying(255),
CONSTRAINT chapter_pkey PRIMARY KEY (id),
CONSTRAINT fk_book FOREIGN KEY (book_id) REFERENCES book(id)
);
INSERT INTO book VALUES (1, 0, 'Book 1');
INSERT INTO book VALUES (2, 0, 'Book 2');
INSERT INTO book VALUES (3, 0, 'Book 3');
INSERT INTO chapter VALUES (1, 1, 'Book 3 Chapter');
CREATE SEQUENCE hibernate_sequence START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
SELECT pg_catalog.setval('hibernate_sequence', 4, true);
为了完整起见,下面是堆栈跟踪:
2018-04-25 16:23:22.462 ERROR --- [nio-8080-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: update or delete on table "book" violates foreign key constraint "fk_book" on table "chapter"
Detail: Key (id)=(1) is still referenced from table "chapter".
2018-04-25 16:23:22.473 ERROR --- [nio-8080-exec-1] org.hibernate.internal.SessionImpl : HHH000346: Error during managed flush [could not execute statement]
2018-04-25 16:23:22.720 ERROR --- [nio-8080-exec-1] o.g.web.errors.GrailsExceptionResolver : PSQLException occurred when processing request: [GET] /test/delete/1
ERROR: update or delete on table "book" violates foreign key constraint "fk_book" on table "chapter"
Detail: Key (id)=(1) is still referenced from table "chapter".. Stacktrace follows:
java.lang.reflect.InvocationTargetException: null
at org.grails.core.DefaultGrailsControllerClass$ReflectionInvoker.invoke(DefaultGrailsControllerClass.java:211)
at org.grails.core.DefaultGrailsControllerClass.invoke(DefaultGrailsControllerClass.java:188)
at org.grails.web.mapping.mvc.UrlMappingsInfoHandlerAdapter.handle(UrlMappingsInfoHandlerAdapter.groovy:90)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
at org.springframework.boot.web.filter.ApplicationContextHeaderFilter.doFilterInternal(ApplicationContextHeaderFilter.java:55)
at org.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(GrailsWebRequestFilter.java:77)
at org.grails.web.filters.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:67)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Caused by: org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [fk_book]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
at org.springframework.orm.hibernate5.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:241)
at org.springframework.orm.hibernate5.HibernateTransactionManager.convertHibernateAccessException(HibernateTransactionManager.java:755)
at org.springframework.orm.hibernate5.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:590)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:765)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:734)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:150)
at grails.gorm.transactions.GrailsTransactionTemplate.execute(GrailsTransactionTemplate.groovy:91)
... 14 common frames omitted
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:112)
at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42)
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:111)
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:97)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:207)
at org.hibernate.engine.jdbc.batch.internal.NonBatchingBatch.addToBatch(NonBatchingBatch.java:45)
at org.hibernate.persister.entity.AbstractEntityPersister.delete(AbstractEntityPersister.java:3311)
at org.hibernate.persister.entity.AbstractEntityPersister.delete(AbstractEntityPersister.java:3548)
at org.hibernate.action.internal.EntityDeleteAction.execute(EntityDeleteAction.java:98)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:586)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:460)
at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:337)
at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39)
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1295)
at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:468)
at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3135)
at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2352)
at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:491)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:147)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$100(JdbcResourceLocalTransactionCoordinatorImpl.java:38)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:231)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:65)
at org.springframework.orm.hibernate5.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:582)
... 18 common frames omitted
Caused by: org.postgresql.util.PSQLException: ERROR: update or delete on table "book" violates foreign key constraint "fk_book" on table "chapter"
Detail: Key (id)=(1) is still referenced from table "chapter".
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2433)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2178)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:306)
at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:441)
at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:365)
at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:155)
at org.postgresql.jdbc.PgPreparedStatement.executeUpdate(PgPreparedStatement.java:132)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204)
... 36 common frames omitted
更新#2,2018年4月25日:有一个Hibernate不知道的数据库表是上述示例的重点。这是一种简单的方法,可以强制数据库级异常,该异常只会被记录,但不会一直到控制器,因此在HTTP级别上,它看起来好像没有发生错误。
更新#3,2018年4月25日:Grails 3.3的另一个测试用例不依赖于任何手动数据库模式"黑客",在同一域(用户)之间具有多对多关系。它由 2 个域类组成:
class User {
String name
static hasMany = [relationships: Relationship]
static mappedBy = [relationships: 'user']
static mapping = { table 'table_user' }
}
class Relationship implements Serializable {
User user
User related
static belongsTo = [ user: User, related: User ]
static mapping = {
table 'table_relationship'
id composite: ['user', 'related']
version false
}
}
还有一个最小的控制器:
class TestController {
@Transactional
def delete(Long id) {
User.get(id).delete()
render "User $id deleted"
}
}
这可以用 PostgreSQL 或 H2 重现,只需使用创建-删除模式和以下 BootStrap 代码创建 2 个用户条目:
def init = { servletContext ->
def user1 = new User(name: 'User 1').save()
new User(name: 'User 2', relationships: [new Relationship(related: user1)]).save()
}
尝试删除用户 2,然后删除用户 1 将正常工作。尝试先删除用户 1 将失败并出现异常,但控制器仍会返回"200 OK"。试试curl http://localhost/test/delete/1
.
为方便起见,您可以在此处下载带有可运行的Grails 3.3.5应用程序的压缩存档:https://www.dropbox.com/s/pycwuxm7r0wyxem/grails3_exception_issue_testapp.zip?dl=0
如果你打算使用@Transactional
你应该使用grails.gorm.transactions.Transactional
而不是grails.transaction.Transactional
。
除此之外,问题似乎不是异常不会到达控制器,而是真正发生的事情是事务直到您调用render
之后才会提交,并且在事务提交之前不会抛出异常。 调用.delete(flush: true)
将显示不同的行为,但无论如何,将事务边界放在控制器中都是一个坏主意。 更好的计划是将数据库交互放在事务性服务中。
我把你的代码放在项目中 https://github.com/jeffbrown/many33。 https://github.com/jeffbrown/many33/commit/f8804c8793b399994c34043c1d340e4bf0d462cd 处的提交显示了组织此代码的更好方法,并产生了不那么令人惊讶的行为。
我希望这有所帮助。