0

I was away from Grails for some time so I tried to create demo rest-api app with some basic domain relationship (one-to-many, many-to-many) and faced some wierd issues. In short, I have 4 domain classes as follows:

class Publisher {
    String name
    static hasMany = [books:Book]
}

class Author {
    String name
    static hasMany = [books: Book]    
}

class Category {
    String name
    static belongsTo = Book
    static hasMany = [books:Book]
}

class Book {
    String title
    Publisher publisher    
    static hasMany = [categories: Category]
}

I'm trying to insert some demo data within bootrap.groovy (with multiple different approaches) but, data that should go into joint tables is not persisted (empty). For example, even when using cascade-create, 'edge' records are persisted (for example categories created from boook) but, there is no data in join table between two of them):

enter image description here

Bootstrap.groovy with different approaches to insert records:

// Stand-alone category
def science = new Category(name: 'science').save(failOnError: true)        

// publishers
def manning = new Publisher(name: 'manning').save(failOnError: true)
def amazon =   new Publisher(name: 'amazon').save(failOnError: true)


// Create single author 
def johnDoe = new Author(name: 'John Doe').save(failOnError: true)

// Create-cascade from Author-Book-Category with explicid 'new' and 'save' Book
def jackDanields = new Author(name: 'Jack Daniels')
    .addToBooks(new Book(title: 'Hate book', publisher: manning).addToCategories(name: 'love').save())
    .addToBooks(new Book(title: 'Fiction book', publisher: manning).addToCategories(name: 'fiction').save())
    .save(failOnError: true)


// Create-cascade from Author-Book without explicit save of book
def zoran = new Author(name: 'Zoran')
    .addToBooks(title: 'First book', publisher: manning)
    .addToBooks(title: 'Second book', publisher: manning)
    .addToBooks(title: 'Third book', publisher: manning)
    .save(failOnError: true)

I tried with both H2 and MariaDb and result is the same. Full project available at github: https://github.com/zbubric/grails4-rest-sample

So, it there any catch that I missed or it is some known issue/feature?

zbubric
  • 363
  • 3
  • 10
  • 2
    You should wrap this inside a transaction, either with a service with @Transactional annotation or with a new Transaction closure Book.withTransaction {...} – Olav Grønås Gjerde Sep 10 '19 at 10:34
  • @OlavGrønåsGjerde thx for tip, wrapping parts into Book.withTransaction {...} does the trick. I'm just curious, do you know about some tutorial regarding transactions (beside official docs). For example, when and what domain to use for withTransaction syntax? Maybe we can continue this on slack channel if you have some time.. – zbubric Sep 18 '19 at 11:37
  • Can you let me know if the code in https://github.com/zbubric/grails4-rest-sample/pull/1 addresses the issue? – Jeff Scott Brown Mar 08 '21 at 14:23
  • I think the issue may be a combination of incompatible dependencies and the fact that you weren't starting a transaction in BootStrap. – Jeff Scott Brown Mar 08 '21 at 14:46

2 Answers2

1

zbubric

Answering to your question

So, it there any catch that I missed or it is some known issue/feature?

I totally agree with Olav's comment that you need to wrap all inserts in BootStrap.groovy into a new transaction to fix the issue.

Let me explain why transaction will enable saving of data in join tables.

In BootStrap.groovy by default there is an open hibernate session (with COMMIT flush mode) but there is no active hibernate transaction.

COMMIT flush mode is important here because with this mode the flush of session is only done on commit of transaction. But in your case no session flush will be done afterwards because there is no active transaction in BootStrap.groovy as I sad before. And no session flush means no saving of entities associations that were added by cascade (also checked it on practice with raw sql queries inside a transaction to the database - had data in join tables only after session flush).

To check if there is a session in BootStrap.groovy (and get more details about it) you can reference

Holders.applicationContext.getBean("sessionFactory").getCurrentSession()

in your code.

To check if there is an active transaction in BootStrap.groovy you can add flush: true to any of the save method calls.

For example:

def science = new Category(name: 'science').save(failOnError: true, flush: true)

In this case you should get an exception:

javax.persistence.TransactionRequiredException: no transaction is in progress

It behaves like that because by default in grails 4.0.0 sessions are configured to not allow flushes without an active transaction.

iVetal
  • 125
  • 5
0

I believe you have your Category>Book relationship wrong as well.

Book has one Category Category can have oneorMany Books

Thus Book can either have a 'Category cat' field or 'belongsTo'; belongsTo should not be in the same place as the 'hasMany'

Orubel
  • 316
  • 4
  • 16
  • "belongsTo should not be in the same place as the 'hasMany'" - In section 5.1.3 at http://gorm.grails.org/7.0.4/hibernate/manual/index.html#gormAssociation we demonstrate a scenario using `belongsTo` and `hasMany` in the same place. – Jeff Scott Brown Mar 05 '21 at 18:02
  • Jeff, Thats with a different domain/table for each. His example (see 'Category' above) is referencing the same domain/table and thats what I was addressing. You cannot use belongsTo and hasMany for the same domain as in above – Orubel Mar 06 '21 at 19:57
  • @JeffScottBrown I honestly hope you as a lead on Grails are not trying to tell this individual he is correct in using 'belongsTo' and 'hasMany' to point to the same Domain is the same Class. Because this throws an error and has for several versions of GORM. – Orubel Mar 07 '21 at 18:10
  • "Because this throws an error and has for several versions of GORM" - I believe the errors in the linked app are due to incompatible dependencies, not the GORM relationships. – Jeff Scott Brown Mar 08 '21 at 14:23
  • @JeffScottBrown The 'belongsTo' can be represented as a variable in the domain thus representing a OneToMany/OnetoOne from the other Domain while the 'hasMany' represents a OnetoMany from this domain. When they point to (and come from) the same domain, you are basically creating an loop. This is why it throws the error and why you create the join table. – Orubel Mar 08 '21 at 16:50
  • @JeffScottBrown I would also point out that Hibernate is mapping the 'belongsTo' and the 'hasMany' both as 'importedKey' to the same table... with the same foreign key name (as it is doing it dynamically); Even though you can reference the 'lazy loaded' table via 'books', the key is still going to be dynamically referenced as 'bookId' for both 'belongsTo' and 'hasMany' thus creating a conflict AFAIK. – Orubel Mar 08 '21 at 17:34
  • @JeffScottBrown If you continue to wonder how Grails maps ManytoMany, take a look at Burt Beckwiths discontinued DatabaseReversEngineer Plugin and how it handles many to many relationships https://grails-plugins.github.io/grails-db-reverse-engineer/ – Orubel Mar 08 '21 at 17:49