无法在 Grails 3 中捕获数据库异常
Unable to catch database exceptions in Grails 3
将应用程序从 Grails 2.5 迁移到 Grails 3.1 后,我遇到了一个奇怪的问题 - 数据库异常永远不会冒泡到控制器。
典型情况如下所示:控制器的删除操作试图删除条目,但数据库级约束阻止了删除。这会导致异常(见下文)——在 Grails 2.5 中,如果未被捕获,这将导致 "Error 500 internal server error"。现在,我们有一个针对这些的自定义映射,因此即使发生未捕获的错误,它也会以用户友好的方式处理,并且用户知道发生了什么。
在 Grails 3 中,这不再有效。记录异常,但永远不会冒泡到控制器。因此,在这种特定情况下,我们的控制器继续运行 returns“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 个 tables,带有 FK 和一些条目。 id 为 1 的书将无法删除,id 为 2 和 3 的书将被成功删除。在所有 3 种情况下,控制器都将 return 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.access0(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 不知道的数据库 table .这是一种强制执行数据库级异常的简单方法,该异常只会被记录下来,但不会一直传递到控制器,因此在 HTTP 级别上,它看起来就像没有发生错误一样。
更新 #3,2018 年 4 月 25 日: Grails 3.3 的另一个测试用例不依赖于任何手动数据库模式 "hacks" 下面是一个同一域(用户)之间的多对多关系。它由 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 重现,只需使用 create-drop 模式和以下 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 将失败并出现异常,但控制器仍将 return “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. The commit at https://github.com/jeffbrown/many33/commit/f8804c8793b399994c34043c1d340e4bf0d462cd 的项目中显示了一种更好的组织此代码的方法,并且产生了不那么令人惊讶的行为。
希望对您有所帮助。
将应用程序从 Grails 2.5 迁移到 Grails 3.1 后,我遇到了一个奇怪的问题 - 数据库异常永远不会冒泡到控制器。
典型情况如下所示:控制器的删除操作试图删除条目,但数据库级约束阻止了删除。这会导致异常(见下文)——在 Grails 2.5 中,如果未被捕获,这将导致 "Error 500 internal server error"。现在,我们有一个针对这些的自定义映射,因此即使发生未捕获的错误,它也会以用户友好的方式处理,并且用户知道发生了什么。
在 Grails 3 中,这不再有效。记录异常,但永远不会冒泡到控制器。因此,在这种特定情况下,我们的控制器继续运行 returns“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 个 tables,带有 FK 和一些条目。 id 为 1 的书将无法删除,id 为 2 和 3 的书将被成功删除。在所有 3 种情况下,控制器都将 return 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.access0(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 不知道的数据库 table .这是一种强制执行数据库级异常的简单方法,该异常只会被记录下来,但不会一直传递到控制器,因此在 HTTP 级别上,它看起来就像没有发生错误一样。
更新 #3,2018 年 4 月 25 日: Grails 3.3 的另一个测试用例不依赖于任何手动数据库模式 "hacks" 下面是一个同一域(用户)之间的多对多关系。它由 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 重现,只需使用 create-drop 模式和以下 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 将失败并出现异常,但控制器仍将 return “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. The commit at https://github.com/jeffbrown/many33/commit/f8804c8793b399994c34043c1d340e4bf0d462cd 的项目中显示了一种更好的组织此代码的方法,并且产生了不那么令人惊讶的行为。
希望对您有所帮助。