无法在Grails 3

时间:2018-04-23 09:58:30

标签: grails gorm

我将应用程序从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关系(删除无操作)到hibernate生成的数据库模式,并尝试通过控制器删除(下面的示例#1),但问题也可能发生在一些多对多关系或其他方案,例如持久化新条目(以及在数据库级别失败)。

UPDATE#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

UPDATE#2,2018年4月25日: Hibernate不知道存在数据库表的事实是上述示例的要点。这是一种强制数据库级异常的简单方法,该异常只会被记录,但不会一直到控制器,所以在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将失败并出现异常,但控制器仍将返回" 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

1 个答案:

答案 0 :(得分:1)

如果您要使用@Transactional,则应使用grails.gorm.transactions.Transactional代替grails.transaction.Transactional

除此之外,问题似乎并不是说例外事件没有达到控制器的程度,因为实际发生的事情是交易不会被提交。在您调用render之后,在提交事务之前不会抛出异常。调用.delete(flush: true)将显示不同的行为,但无论如何将事务边界放在控制器中都是一个坏主意。更好的计划是将数据库交互放在事务性服务中。

我将您的代码放在enter image description here的项目中。 https://github.com/jeffbrown/many33处的提交显示了一种更好的方法来组织此代码并产生不太令人惊讶的行为。

我希望有所帮助。