idonaldson
idonaldson

Reputation: 475

Multiple chained saves with GORM

We are trying to save multiple domain objects that are chained together with foreign key constraints and are having trouble getting GORM to work.

Domain classes:

class Degree {

/* Default (injected) attributes of GORM */
Long    id
Long    version

/* Automatic timestamping of GORM */
Date    dateCreated
Date    lastUpdated

String department
String subject
String catalogYear
String level
String type
Long   totalCredits
Double cumulativeGPArequired    
String capstone
String comment
String updatedBy

static hasMany = [degreeBlocks: DegreeBlock]    // tells GORM to associate other domain objects for a 1-n mapping

static mapping = {
    sort "id"
   version false
   table 'degree'
   columns{
           id column: 'id'
           department column: 'department'
           subject column: 'subject'
           catalogYear column: 'catalog_year'
           level column: 'degree_level'
           type column: 'type'
           totalCredits column: 'total_credits'
           cumulativeGPArequired column: 'cume_gpa_req'
           capstone column: 'capstone'
           comment column: 'degree_comment'                
           dateCreated column: 'date_created'
           lastUpdated column: 'last_updated'
           updatedBy column: 'updated_by'
   }
   id generator:'sequence', params:[sequence:'degree_id_sequence']
}   

static constraints = {
    department(nullable: true, maxSize: 30)
    subject(nullable: true, maxSize: 30 )
    catalogYear(nullable: true, maxSize: 9 )
    level(nullable: true, maxSize: 15)
    type(nullable: true, maxSize: 30)
    totalCredits(nullable: true, maxSize: 3)
    cumulativeGPArequired(nullable: true, maxSize: 3)       
    capstone(nullable: true, maxSize: 30)
    comment(nullable: true, maxSize: 2000)
    lastUpdated(nullable: true)
    updatedBy(nullable: true)
}

@Override   // Override toString for a nicer / more descriptive UI 
public String toString() {
    return "${subject}";
}
}

class DegreeBlock {

/* Default (injected) attributes of GORM */
Long    id
Long    version

/* Automatic timestamping of GORM */
Date    dateCreated
Date    lastUpdated

String  blockType
String  comment


static hasMany   = [courseBlocks: CourseBlock]  // TELLS GORM TO ASSOCIATE OTHER DOMAIN OBJECTS FOR A 1-N MAPPING
static belongsTo = [degree: Degree] // TELLS GORM TO CASCADE COMMANDS: E.G., DELETE THIS OBJECT IF THE "PARENT" IS DELETED.

static mapping = {
    sort "id"
    version true
    table 'degree_block'
    columns{
           id column: 'id'
           level column: 'degree_level'
           blockType column: 'block_type'
           comment column: 'block_comment'
           dateCreated column: 'date_created'
           lastUpdated column: 'last_updated'
   }
    id generator:'sequence', params:[sequence:'degree_block_id_sequence']
}      
 static constraints = {
     blockType(nullable: true, maxSize: 50)
     comment(nullable: true, maxSize: 2000)
     lastUpdated(nullable: true)
   }


/*
 * Methods of the Domain Class
 */
@Override   // Override toString for a nicer / more descriptive UI 
public String toString() {
    return "${blockType}";
}
}

class CourseBlock {

/* Default (injected) attributes of GORM */
Long    id
Long    version

/* Automatic timestamping of GORM */
Date    dateCreated
Date    lastUpdated

String name
String rule
String credits
String eval
String prereqNotes
String comment

static hasMany = [courses: Course] // tells GORM to associate other domain objects for a 1-n mapping
static belongsTo = [degreeBlock: DegreeBlock] // tells GORM to cascade commands: e.g., delete this object if the "parent" is deleted.

static mapping = {
    sort "id"
    version false
    table 'degree'
    columns{
           id column: 'id'
           name column: 'name'
           rule column: 'rule'
           credits column: 'credits'
           eval column: 'eval'
           prereqNotes column: 'prereq_notes'
           comment column: 'course_comment'
           dateCreated column: 'date_created'
           lastUpdated column: 'last_updated'
   }
   id generator:'sequence', params:[sequence:'course_block_id_sequence']
}


static constraints = {
    name(nullable: true, maxSize: 80)
    rule(nullable: true, maxSize: 80 )
    credits(nullable: true, maxSize: 10 )
    eval(nullable: true, maxSize: 4)
    prereqNotes(nullable: true, maxSize: 2000)
    comment(nullable: true, maxSize: 2000)
    lastUpdated(nullable: true)
}

/*
 * Methods of the Domain Class
 */
@Override   // Override toString for a nicer / more descriptive UI 
public String toString() {
    return "${name}";
}
}

class Course {

/* Default (injected) attributes of GORM */
Long    id
Long    version

/* Automatic timestamping of GORM */
Date    dateCreated
Date    lastUpdated

String subject
String courseNumber
String title
String credits
String term

static belongsTo = [courseBlock:CourseBlock] // tells GORM to cascade commands: e.g., delete this object if the "parent" is deleted.

static mapping = {
    sort "id"
   version false
   table 'degree'
   columns{
           id column: 'id'
           subject column: 'subject_code'
           courseNumber column: 'course_number'
           title column: 'title'
           credits column: 'credits'
           term column: 'term'
           dateCreated column: 'date_created'
           lastUpdated column: 'last_updated'
   }
   id generator:'sequence', params:[sequence:'course_id_sequence']
}

static constraints = {
    subject(nullable: true, maxSize: 5 )
    courseNumber(nullable: true, maxSize: 5 )
    title(nullable: true, maxSize: 80)
    credits(nullable: true, maxSize: 8)
    term(nullable: true, maxSize: 10)
    lastUpdated(nullable: true)
}

/*
 * Methods of the Domain Class
 */
@Override   // Override toString for a nicer / more descriptive UI 
public String toString() {
    return "${subject} ${courseNumber}";
}
}

Service method code:

def saveDegree(params){
    def degree = new Degree(params).save()
    def degreeBlock = new DegreeBlock(params, degree:degree)
    degree.addToDegreeBlocks(degreeBlock)
    degreeBlock.save(flush:true)
    def courseBlock = new CourseBlock(params, degreeBlock:degreeBlock)
    degreeBlock.addToCourseBlocks(courseBlock)
    courseBlock.save(flush:true)
    def course = new Course(params, courseBlock:courseBlock)
    courseBlock.addToCourses(course)
    course.save(flush:true)

    return degree
}

We tried following along here: How do I save GORM objects with multiple many-to-one relationships?

But it gives us the error:

| Error 2013-09-12 11:03:12,421 [http-bio-8080-exec-10] ERROR util.JDBCExceptionReporter  - ORA-02291: integrity constraint (FK1_DEGREE_ID) violated - parent key not found

| Error 2013-09-12 11:03:12,424 [http-bio-8080-exec-10] ERROR events.PatchedDefaultFlushEventListener  - Could not synchronize database state with session
Message: Could not execute JDBC batch update
Line | Method
->>   49 | saveDegree in degreebuilder.DegreebuilderService
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
|     50 | save       in degreebuilder.DegreeController
|    195 | doFilter . in grails.plugin.cache.web.filter.PageFragmentCachingFilter
|     63 | doFilter   in grails.plugin.cache.web.filter.AbstractFilter
|   1145 | runWorker  in java.util.concurrent.ThreadPoolExecutor
|    615 | run        in java.util.concurrent.ThreadPoolExecutor$Worker
^    724 | run . . .  in java.lang.Thread
Caused by BatchUpdateException: ORA-02291: integrity constraint (FK1_DEGREE_ID)  violated - parent key not found

->>  343 | throwBatchUpdateException in oracle.jdbc.driver.DatabaseError
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
|   10698 | executeBatch in oracle.jdbc.driver.OraclePreparedStatement
|    297 | executeBatch in org.apache.commons.dbcp.DelegatingStatement
|     49 | saveDegree in degreebuilder.DegreebuilderService
|     50 | save . . . in degreebuilder.DegreeController
|    195 | doFilter   in grails.plugin.cache.web.filter.PageFragmentCachingFilter
|     63 | doFilter . in grails.plugin.cache.web.filter.AbstractFilter
|   1145 | runWorker  in java.util.concurrent.ThreadPoolExecutor
|    615 | run . . .  in java.util.concurrent.ThreadPoolExecutor$Worker
^    724 | run        in java.lang.Thread

Walking through it with the debugger, it seems that the initial degree isn't created until the return statement at the bottom of the method, even though I thought (flush:true) was supposed to force the persistance?

I'm just trying to do the saves the easiest and most correct way possible, and thought GORM could handle it this way, but maybe not.

Upvotes: 0

Views: 466

Answers (1)

dmahapatro
dmahapatro

Reputation: 50245

Your case is one-to-many instead of many-to-one.

Try this instead. The parent has to be persisted first and eventually the children, you do not need to flush the children every time it is associated to its parent but finally the root parent can be flushed to witness the cascade behavior.

def saveDegree(params){
    def degree = new Degree(params)

    def degreeBlock = new DegreeBlock(params)
    degree.addToDegreeBlocks(degreeBlock)

    def courseBlock = new CourseBlock(params)
    degreeBlock.addToCourseBlocks(courseBlock)

    def course = new Course(params)
    courseBlock.addToCourses(course)

    degree.save(flush:true)

    return degree
}

Upvotes: 1

Related Questions