Grails: How to insert with many-to-many with extra column? - grails

Take the example of author, book and with a join table author_books with an extra royalty column:
CREATE TABLE 'author_books' (
'author_id` bigint(20) NOT NULL,
'book_id' bigint(20) NOT NULL,
'royalty' int NOT NULL,
PRIMARY KEY ('author_id', 'book_id') ,
INDEX 'FK24C812F6183CFE1B' ('book_id'),
INDEX 'FK24C812F6DAE0A69B' ('author_id')
)
If I don't have the db-reverse-engineer plugin generate the AuthorBooks class, the generated code would be:
class Author {
String name
static hasMany = [books: Book]
}
class Book {
String title
static hasMany = [authors: Author]
static belongsTo = [Author]
}
The following code:
def author1 = new Author(name: 'author1').addToBooks(new Book(title:
'book1')).save(flush: true)
would insert one row into author table and one row in book table, but would fail to insert into author_books because royalty cannot be null.
However if have the db-reverse-engineer plugin generate the AuthorBooks class, the generated code would be:
class Author {
String name
static hasMany = [authorBookses: AuthorBooks]
}
class Book {
String title
static hasMany = [authorBookses: AuthorBooks]
}
class AuthorBooks implements Serializable {
Long authorId
Long bookId
Integer royalty
Author author
Book book
int hashCode() {
def builder = new HashCodeBuilder()
builder.append authorId
builder.append bookId
builder.toHashCode()
}
boolean equals(other) {
if (other == null) return false
def builder = new EqualsBuilder()
builder.append authorId, other.authorId
builder.append bookId, other.bookId
builder.isEquals()
}
static belongsTo = [author: Author, book: Book]
static mapping = {
author insertable: false // these insertable and updateable codes were manually added
author updateable: false // otherwise it would not run
book insertable: false
book updateable: false
id composite: ["authorId", "bookId"]
version false
}
}
In this case, I cannot call author.addToAuthorBooks(new AuthorBooks( )) because author_books cannot have author_id or book_id to be null. In the end I need to do the following to get it work:
def author1 = new Author(name: 'author1').save(flush: true)
def book1 = new Book(title: 'book1').save(flush: true)
def authorbook1 = new AuthorBooks(authorId: author1.id, bookId: book1.id,
royalty: 50, author: author1, book: book1).save(flush: true)
which is acceptable to me. But then what is the benefits of having hasMany association in the Author and Book class? Any better way to do this? Ideally, something like following would be cool
def author1 = new Author(name: 'author1').addToBooks(book: new Book(title: 'book1'),
royalty: 50).save(flush: true)

There are some changes that I would like to recommend,specifically in your AuthorBooks domain:
class Author {
String name
static hasMany = [authorBookses: AuthorBooks]
}
class Book {
String title
static hasMany = [authorBookses: AuthorBooks]
}
class AuthorBooks implements Serializable {
Integer royalty
Author author
Book book
int hashCode() {
def builder = new HashCodeBuilder()
builder.append author.id
builder.append book.id
builder.toHashCode()
}
boolean equals(other) {
if (other == null) return false
def builder = new EqualsBuilder()
builder.append author.id, other.author.id
builder.append book.id, other.book.id
builder.isEquals()
}
static belongsTo = [author: Author, book: Book]
static mapping = {
id composite: ["author", "book"]
version false
}
}
Having additional ids for book and author is redundant. I'm not 100% sure about what I'm going to suggest, but it would be worth of trying the following:
def author1 = new Author(name: 'author1', authorBookses: new HashSet())
def book1 = new Book(title: 'book1', authorBookses: new HashSet())
def authorbook1 = new AuthorBooks(royalty: 50, author: author1, book: book1)
author1.authorBookses(authorbook1)
book1.authorBookses(authorbook1)
authorbook1.save(flush: true)

Related

Grails, gorm. Find child by parent and parent by child

For example, I've parent class Author:
class Author {
String name
static hasMany = [
fiction: Book,
nonFiction: Book
]
}
and a child class Book:
class Book {
String title
static belongsTo = [author: Author]
}
I've done some records to Author using:
def fictBook = new Book(title: "IT")
def nonFictBook = new Book(title: "On Writing: A Memoir of the Craft")
def a = new Author(name: "Stephen King")
.addToFiction(fictBook)
.addToNonFiction(nonFictBook)
.save()
How can I found child-class record by parent and parent-class record by child?
In my opinion, this is not the best way to model your data. I would do it like this.
class Author {
String name
static hasMany = [books: Book]
}
class Book {
String title
BookTypes bookType
static belongsTo = [author: Author]
}
enum BookTypes {
FICTION,
NON_FICTION
}
Then, you can do lookups like
def author = Author.get(1)
def nonFictionByAuthor = Book.findAllByAuthorAndBookType(author, BookTypes.NON_FICTION)
You could also just do something like this...
def author = Author.get(1)
def fictionBooks = author.books.findAll { it.bookType == BookTypes.FICTION }
And then inversely:
def fictionBook = Book.findByTitleAndBookType('Title001', BookTypes.FICTION)

grails domain dirtyPropertyNames for child object

def names = domain.dirtyPropertyNames
for (name in names) {
def originalValue = domain.getPersistentValue(name)
def newValue = domain."$name"
}
But if i have a relation of 1-1 with other domain
how can i access dirtyPropertyNames for that other domain
def dirtyProperties = domain?.otherDomain?.dirtyPropertyNames
for (name in dirtyProperties ) {
def originalValue = domain?.otherDomain?.getPersistentValue(name)
def newValue = domain?.otherDomain?."$name"
}
But i am getting
No such property: dirtyPropertyNames for class: otherDomain
This seems not to be an issue when tested against Grails 2.2.4 and 2.3.0.
How have you tailored the 1:1 relationship?
Here is a sample, hope that helps:
class Book {
String name
String isbn
static hasOne = [author: Author]
}
class Author {
String name
String email
Book book
}
//Save new data
def book = new Book(name: 'Programming Grails', isbn: '123')
book.author = new Author(name: "Burt", email: 'test', book: book)
book.save(flush: true)
//Sanity check
println Book.all
println Author.all
//Check dirty properties of association
def book = Book.get(1)
book.author.name = 'Graeme'
def dirtyProperties = book?.author?.dirtyPropertyNames
for (name in dirtyProperties ) {
println book?.author?.getPersistentValue(name) //Burt
println book?.author?."$name" //Graeme
}
Although, wrt Grails 2.3.0 you can persist a 1-1 relation as done below unlike above:
def author = new Author(name: "Burt", email: 'test')
def book = new Book(author: author, name: 'PG', isbn: '123').save(flush: true)

Grails criteria select when hasMany hasn't any elements

I have the classes:
class Course{
String name
static hasMany = [
studentGrades: StudentGrade
]
}
class StudentGrade{
String name
int grade
}
How can I make a criteria to get the courses without any student grade?
You could use the isEmpty criterion method:
def c = Course.createCriteria()
def results = c.list {
isEmpty("studentGrades")
}
See the docs for further informations.

Assigned domain class id wierdness with Grails

I want to set the id manually on grails 1.3.7
This compiles but the id is always 0
//in bootstrap
def it1 = new ItemType(id:4,name:'feature')
it1.save()
//domanin class
class ItemType {
String name
int id
static constraints = {
id(unique:true,blank:false)
name(blank:false)
}
static mapping = {
id column: 'ItemTypeId', generator:'assigned'
name column: 'Name'
version false
}
}
This compiles and id 4 (as required)
//in bootstrap
def it1 = new ItemType(name:'feature')
it1.id=4
it1.save()
//domanin class
class ItemType {
String name
//int id
static constraints = {
id(unique:true,blank:false)
name(blank:false)
}
static mapping = {
id column: 'ItemTypeId', generator:'assigned'
name column: 'Name'
version false
}
}
So my question is there a way to have id as prop but assigned?
Had the same problem a few days ago: my own id in GORM
It seems that it is a feature :-)

Grails one-to-many mapping with joinTable

I have two domain-classes. One is a "Partner" the other is a "Customer". A customer can be a part of a Partner and a Partner can have 1 or more Customers:
class Customer {
Integer id
String name
static hasOne = [partner:Partner]
static mapping = {
partner joinTable:[name:'PartnerMap',column:'partner_id',key:'customer_id']
}
}
class Partner {
Integer id
static hasMany = [customers:Customer]
static mapping = {
customers joinTable:[name:'PartnerMap',column:'customer_id',key:'partner_id']
}
}
However, whenever I try to see if a customer is a part of a partner, like this:
def customers = Customer.list()
customers.each {
if (it.partner) {
println "Partner!"
}
}
I get the following error:
org.springframework.dao.InvalidDataAccessResourceUsageException: could not execute query; SQL [select this_.customer_id as customer1_162_0_, this_.company as company162_0_, this_.display_name as display3_162_0_, this_.parent_customer_id as parent4_162_0_, this_.partner_id as partner5_162_0_, this_.server_id as server6_162_0_, this_.status as status162_0_, this_.vertical_market as vertical8_162_0_ from Customer this_]; nested exception is org.hibernate.exception.SQLGrammarException: could not execute query
It looks as if Grails is thinking partner_id is a part of the Customer query, and it's not... It is in the PartnerMap table, which is supposed to find the customer_id, then get the Partner from the corresponding partner_id.
Anyone have any clue what I'm doing wrong?
Edit: I forgot to mention I'm doing this with legacy database tables. So I have a Partner, Customer and PartnerMap table. PartnerMap has simply a customer_id and partner_id field.
Given the way 1-many works when you want a join table, I think it's not possible with standard GORM to make it bidirectional and access a Customer's Partner. But you can map the join table with a domain class and access things that way:
Customer:
class Customer {
String name
def getPartner() {
PartnerMap.findByCustomer(this)?.partner
}
}
Partner:
class Partner {
String name
def getCustomers() {
PartnerMap.findAllByPartner(this)*.customer
}
}
PartnerMap:
import org.apache.commons.lang.builder.HashCodeBuilder
class PartnerMap implements Serializable {
Partner partner
Customer customer
boolean equals(other) {
if (!(other instanceof PartnerMap)) {
return false
}
other.partner?.id == partner?.id &&
other.customer?.id == customer?.id
}
int hashCode() {
def builder = new HashCodeBuilder()
if (partner) builder.append(partner.id)
if (customer) builder.append(customer.id)
builder.toHashCode()
}
static PartnerMap get(long partnerId, long customerId) {
find 'from PartnerMap where partner.id=:partnerId and customer.id=:customerId',
[partnerId: partnerId, customerId: customerId]
}
static PartnerMap create(Partner partner, Customer customer, boolean flush = false) {
new PartnerMap(partner: partner, customer: customer).save(flush: flush, insert: true)
}
static boolean remove(Partner partner, Customer customer, boolean flush = false) {
PartnerMap instance = PartnerMap.findByPartnerAndCustomer(partner, customer)
instance ? instance.delete(flush: flush) : false
}
static void removeAll(Partner partner) {
executeUpdate 'DELETE FROM PartnerMap WHERE partner=:partner', [partner: partner]
}
static void removeAll(Customer customer) {
executeUpdate 'DELETE FROM PartnerMap WHERE customer=:customer', [customer: customer]
}
static mapping = {
id composite: ['customer', 'partner']
version false
table 'PartnerMap'
}
}
Since you're not using hasMany, you lose the addToXXX dynamic method, but you can call PartnerMap.create() to relate two instances. You also lose the collection and back-ref in the domain classes, but I added utility methods for those.

Resources