grails / grails-core

The Grails Web Application Framework
http://grails.org
Apache License 2.0
2.79k stars 950 forks source link

domain with the structure of header-item failed to save in Grails 3.3.8 #11077

Open wureka opened 6 years ago

wureka commented 6 years ago

Environment Information

Domains

class Header {
    String headerNo
    Date dateCreated
    Date lastUpdated
    static hasMany = [items: Item]
    static constraints = {
        items   nullable: true
    }
}

class Item {
    int itemNo
    Date dateCreated
    Date lastUpdated
    static belongsTo = [header: Header]
    static constraints = {
        itemNo  unique: 'header'
    }
}

application.groovy is below:

grails.gorm.default.mapping = {
    version         false
    createdBy       length: 50
    updatedBy       length: 50
    dateCreated     sqlType:'timestamp with time zone'
    lastUpdated     sqlType:'timestamp with time zone'
    autoTimestamp   true
}

My database config in application.yml:

environments:
    development:
        dataSource:
            dbCreate: create-drop
            # url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
            dialect: org.hibernate.dialect.PostgreSQLDialect
            driverClassName: org.postgresql.Driver
            url: jdbc:postgresql://localhost/testdb
            username: user
            password:  user

build.gradle:

dependencies {
   .... // original configures

    compile "org.postgresql:postgresql:9.4.1212.jre7"
}

My TestController:

class TestController {
    def createHeaderCase1() {
        Header header
        Header.withTransaction {
            header = new Header(headerNo: 'HEADER_1')
            header.addToItems(new Item(itemNo: 1))
            header.addToItems(new Item(itemNo: 2))
            header.save()

        }
        println "header dateCreated:${header.dateCreated}"
        render "createHeaderCase1 test done"
    }

    def createHeaderCase2() {
        Header header
        Header.withTransaction {
            header = new Header(headerNo: 'HEADER_1')
            header.save()
            header.addToItems(new Item(itemNo: 1))
            header.addToItems(new Item(itemNo: 2))
            header.save()

        }
        println "header dateCreated:${header.dateCreated}"
        render "createHeaderCase2 test done"
    }

    def createHeaderCase3() {
        Header.withTransaction {
            Header header1 = new Header(headerNo: 'HEADER_1')
            header1.addToItems(new Item(itemNo: 1))
            header1.addToItems(new Item(itemNo: 2))
            header1.save()

            Header header2 = new Header(headerNo: 'HEADER_2')
            header2.addToItems(new Item(itemNo: 1))
            header2.addToItems(new Item(itemNo: 2))
            header2.save()

        }
        render "createHeaderCase3 test done"
    }
}

Problems:

  1. execute http://localhost:8080/test/createHeaderCase1. It will success for the first time, and will fail for 2nd time.
  2. execute http://localhost:8080/test/createHeaderCase2. the result depends on what database you use: 2.1 If you use h2, it will success. 2.2 If you use PostgreSQL(v10), it will lead below exception: ERROR: null value in column "date_created" violates not-null constraint 詳細:Failing row contains (1, null, HEADER_1, 2018-08-23 08:23:53.534+08).
  3. execute http://localhost:8080/test/createHeaderCase3. It will success for the first time, and will fail for 2nd time.

However, all above tests will pass when using Grails 3.2.8 (I didn't test 3.2.9~3.2.11). I think the behaviors of Grails 3.2,8 should be correct. If I am right, could Grails team fix that bug? That's very important.

Thanks

Example Application

example app with Grails 3.3.8 web338.zip

example app with Grails 3.2.8 web328.zip

zyro23 commented 6 years ago

10964 maybe?

jeffscottbrown commented 6 years ago

In TestController. createHeaderCase1 if you replace header = new Header(headerNo: 'HEADER_1') with header = new Header(headerNo: 'HEADER_1').save(), I expect the problem in that method may go away. It does go away with H2. I am not setup to test with postgres right now. If you can test that and provide feedback, that would be appreciated.

jeffscottbrown commented 6 years ago

A significant detail that is not mentioned in the original description is the fact that failOnError is set to true in application.yml, which affects the relevant behavior. If that isn't set, the controller action will appear to work because the code isn't checking the return value when calling .save().

The project at https://github.com/jeffbrown/issue11077 contains an app which demonstrates the behavior but eliminates a lot of unrelated stuff in the originally linked projects.

The relevant bits:

https://github.com/jeffbrown/issue11077/blob/master/grails-app/domain/issue11077/Header.groovy

package issue11077

class Header {
    String headerNo
    static hasMany = [items: Item]
}

https://github.com/jeffbrown/issue11077/blob/master/grails-app/domain/issue11077/Item.groovy

package issue11077

class Item {
    int itemNo
    static belongsTo = [header: Header]
    static constraints = {
        itemNo  unique: 'header'
    }
}

https://github.com/jeffbrown/issue11077/blob/master/grails-app/controllers/issue11077/DemoController.groovy

package issue11077

class DemoController {

    def index() {
        Header.withTransaction {
            // Saving the Header before adding
            // items allows the integration test to pass
            Header h = new Header(headerNo: 'SOMETHING')//.save()
            h.addToItems itemNo: 1
            h.addToItems itemNo: 2
            h.save(failOnError: true)
        }
        render 'Success'
    }
}

https://github.com/jeffbrown/issue11077/blob/master/src/integration-test/groovy/issue11077/DemoControllerSpec.groovy

package issue11077

import geb.spock.GebSpec
import grails.testing.mixin.integration.Integration

@Integration
class DemoControllerSpec extends GebSpec {

    void 'test that the index action may be invoked multiple times'() {
        when:
        go '/demo/index'

        then:
        $().text() == 'Success'

        when:
        go '/demo/index'

        then:
        $().text() == 'Success'
    }
}

That integration test will fail. If DemoController is modified to save the Header before adding the items, the test will pass.

wureka commented 6 years ago

@jeffbrown I have tried your suggestion as below: header = new Header(headerNo: 'HEADER_1').save()

when using PostgreSQL V10, the below exception always appears since first trial:

o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: null value in column "date_created" violates not-null constraint

When using H2, it will pass no matter how many trials I did.

By the way, I have set failOnError to true in application.yml:

grails:
    profile: web
    codegen:
        defaultPackage: web338
    gorm:
        failOnError: true
        reactor:
            # Whether to translate GORM events into Reactor events
            # Disabled by default for performance reasons
            events: false
jeffscottbrown commented 6 years ago

By the way, I have set failOnError to true in application.yml

I noticed that and commented on that in an earlier comment.

Thanks again.

ilopmar commented 6 years ago

@wureka as @zyro23 mentioned, keep in mind that the null value in column "date_created" violates not-null constraint error in postgres is because of #10964. I sent a PR to fix it and it will be included in the next GORM version.