I keep getting error after error when trying to map a many to many relationship between two tables where the joinTable has a composite id. I have three domains. One of them is a join table with composite id, the other two have a many to many relationship between each other through this intermediary table.
Demo Code on Github
Foreign key error
I'm upgrading an internal project to the last version of Grails, and I'm fixing the errors as they show up. I'm stuck with the error below but I still don't know the cause of it. In the demo code I get the same error only when static belongsTo = [areas: Professional] is set on the Area domain:
Foreign key (professional_area [])) must have same number of columns as the referenced primary key (area [id_area])
Query errors
If I remove the line with belongsTo in the Area domain, most of the code works, but I still get an error when trying to use Area.findAllByProfessionalsInList, for instance:
2022-08-30 15:27:26.930 ERROR --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Parameter "#1" is not set; SQL statement:
select this_.id_area as id_area1_0_0_, this_.name as name2_0_0_ from area this_ where this_.id_area in (?) [90012-200]
org.hibernate.exception.DataException: could not extract ResultSet
ProfessionalArea.findAll() returns an empty list
Area:
class Area implements Serializable {
String id
String name
static belongsTo = [areas: Professional]
static hasMany = [
professionals: Professional,
]
static mapping = {
id column: "id_area", generator: "assigned"
version false
professionals joinTable: [name: 'professional_area', key: 'id_area']
}
}
Professional:
class Professional implements Serializable {
String name
static hasMany = [
areas: Area,
]
static mapping = {
id column: "id_professional", generator: "assigned"
tablePerHierarchy false
version false
areas joinTable: [name:'professional_area', key: 'id_professional']
}
}
ProfessionalArea:
import org.apache.commons.lang.builder.HashCodeBuilder
class ProfessionalArea implements Serializable {
String activities
Area area
Professional professional
static mapping = {
table name: "professional_area"
id composite: ['professional', 'area']
version false
professional column : 'id_professional', lazy: false
area column: 'id_area', lazy: false
}
}
Ideally, as I understand, the domains mapped should pass the following test (also on the demo code):
void "composite key working"() {
when:
def prof = Professional.findById(1395)
def area = Area.findById("TEST_AREA")
def area2 = Area.findById("TEST_AREA2")
then: "Professional is in 2 areas"
prof.areas.size() == 2
and: "professional contains area"
prof.areas.containsAll([area, area2])
and: "ProfessionalArea Contains 2 rows"
ProfessionalArea.count() == 2
and: "Can find area by professionals"
Area.findAllByProfessionalsInList([prof], [fetch: [professionals: 'join']]).size() == 2
and: "Can find Professionals by area"
Professional.findByAreasInList([area], [fetch: [areas: 'join']])?.id == 1395
and: "Can get Professionals through joinTable ProfessionalArea"
1395 in ProfessionalArea.findAllByArea(area2)*.professional*.id
and: "Can get Area through joinTable ProfessionalArea"
"TEST_AREA2" in ProfessionalArea.findAllByProfessional(prof)*.area*.id
}
Related
I have two domain classes with many to many relationship with extra column.I created below domain classes by following the logic from the forums and still face an issue in saving the data in additional domain class.Roylaty is the additional column to save the value in the mapping table.
Below are the 3 domain classes:
class AuthorBook implements Serializable {
Author author
Book book
String royalty
boolean equals(other) {
if (!(other instanceof AuthorBook)) {
return false
}
other.author?.id == author?.id &&
other.book?.id == book?.id
}
int hashCode() {
def builder = new HashCodeBuilder()
if (author) builder.append(author.id)
if (book) builder.append(book.id)
builder.toHashCode()
}
static AuthorBook get(long authorId, long bookId) {
find 'from AuthorBook where author.id=:authorId and book.id=:bookId',
[authorId: authorId, bookId: bookId]
}
static AuthorBook create(Author author, Book book, boolean flush = false) {
new AuthorBook(author: author, book: book).save(flush: flush, insert: true)
}
}
class Author implements Serializable{
string name(nullable:false,unique:true)
Set<Book> getBooks() {
AuthorBook.findAllByAuthor(this).collect { it.book } as Set
}
}
class Book implements Serializable{
string title(nullable:false,unique:true)
Set<Author> getAuthors() {
AuthorBook.findAllByBook(this).collect { it.author } as Set
}
}
In one of my controllers i wrote the below logic:
def author1 = new Author("ABC")
author.save(flush:true)
def book1= new Book("GORM")
book.save(flush:true)
def authorBook = new AuthorBook(royalty:100,author:author1,book:book1)
authorBook.save(flush:true)
For both author and book, it works as expected i.e it won't allow duplicates and in the mapping table too. it won't allow duplicates but I want the output to be as below in the mapping table
Author AuthorBook Book
id Name id author_id book_id royalty id title
1 XYZ 1 1 1 500 1 Gorm
2 1 1 1000
It won't save this value as it is considering the combination of author_id and book_id to be unique even though I did not set any composite key on id's in the mapping table.
What should I change in the mapping table to allow duplicates?
Can you manually insert that row into the database? I suspect this is caused by your implementation of equals and hashcode on AuthorBook.
These two objects are the same:
author=1;book=1;royalty=100 and author=1;book=1;royalty=500 because your equality methods are only comparing author and book.
I have two domain classes and the relationship between them is 1 to Many. I know how to map the columns of each class to their respective tables individually but how do I map the relationship that exists in my MSSQL database? There is no join table and I only have read-only access. I've looked at various pages of the Grails documentation and this is where I am at at the moment (One student has many courses). In my tables the foreign key that ties the two tables together is in the Courses table.
class StudentHeader { //on the one side
String stuNo
String stuName
String stuStreet
static mappedBy = [refs: "fkCustNo"]
static hasMany = [refs: CourseHeader]
static constraints = {
}
static mapping = {
table name: "[tbl_Students]", schema: "[dbo]", catalog: "[CRD].[CourseTrak]"
version false
id generator: 'assigned', name: 'stuNo'
stuNo column: '[PK_StudentNo]'
stuName column: '[Student_Name]'
stuStreet column: '[Student_Street]'
}
}
class CourseHeader { //on the many side
String courId
String courName
StudentHeader fkCourNo
static constraints = {
}
static mapping = {
table name: "[tbl_Courses]", schema: "[dbo]", catalog: "[CRD].[CourseTrak]"
version false
id generator: 'assigned', name: 'courId'
courId column: '[PK_CourseId]'
courName column: '[Course_Name]'
fkCourNo column: '[FK_CourseNo]'
}
}
As a test here is how I am trying to access the courses of a student
StudentHeader.first().refs
I believe I have it figured out. For the domain class on the many side you need to set insert and updateable equal to false like so (The object field in your "many" domain has to have the same value as the key-value pair in mappedBy):
class StudentHeader{
static mappedBy = [refs: "fkCustNo"]
}
class CourseHeader { //on the many side
String courId
String courName
StudentHeader fkCustNo
static constraints = {
}
static mapping = {
table name: "[tbl_Courses]", schema: "[dbo]", catalog: "[CRD].[CourseTrak]"
version false
id generator: 'assigned', name: 'courId'
courId column: '[PK_CourseId]'
courName column: '[Course_Name]'
fkCustNo column: '[FK_CourseNo]', insertable: false, updateable: false
}
}
So I have my domain below. Basically what I'm wanting to do is get all the demandNumbers for a given WorkOrderSummary. The data structure exists in the view already so I really just need to figure how I can map the WorkOrder-to-demandNumbers (one-to-many) relationship. So one lab_order_header_id can contain many demand_header_ids.
class WorkOrderSummary {
String workOrderNumber
Long demandNumbers
String demandTypeName
String statusName
Date needByDate
String customerName
Long facilityId
Long labDestinationId
Long assetTagQuantity
static hasMany = [demandNumbers: WorkOrderSummary]
static mapping = {
version false
table name: 'work_orders_v', schema: 'lab'
id column: 'lab_order_header_id'
demandNumbers column: 'demand_header_id'
demandTypeName column: 'demand_type'
statusName column: 'status'
}
}
Right now this is only getting me one-to-one in terms of lab_order_header_id to demand_header_ids.
Any suggestions?
Edit - 9/26/2016
Created another domain class for the demandNumbers and notated the belongsTo:
class SalesOrderSummary {
String demandTypeName
static belongsTo = [workOrder: WorkOrderSummary]
static constraints = {
}
static mapping = {
version false
table name: 'work_orders_v', schema: 'lab'
id column: 'demand_header_id'
workOrder column: 'lab_order_header_id'
demandTypeName column: 'demand_type'
}
}
If you wish to have many DemandNumbers for a given WorkOrderSummary
you should replace the following line:
static hasMany = [demandNumbers: WorkOrderSummary]
With the following line:
static hasMany = [demandNumbers: DemandNumbers]
It should make the relationship you are trying to get.
I have a domain class:
class Author {
String name
static hasMany = [superFantasticAndAwesomeBooks: Book, superBadAndUltraBoringBooks: Book]
}
This is all nice when using the in-memory database, however, when running on Oracle the Book collections are modeled in a join table which cannot be created because the column names are too long.
So, then I tried specifying the join table properties:
static mapping = {
superFantasticAndAwesomeBooks joinTable: [key: awesomeBooks]
superBadAndUltraBoringBooks joinTable: [key: boringBooks]
}
The problem (which doesn't happen if joinTable isn't specified) is that the join table is created where columns correspoinding to awesomeBooks and boringBooks are NOT NULL (they need to be nullable because a Book will be an awesomeBook or a boringBook)
Is there any way to configure joinTable to allow NULL columns?
Another option would be to map the join table yourself with a Domain class, for example:
class AuthorBook {
Author author
Book book
String status
static constraints = {
author(nullable:false)
book(nullable:false)
status(nullable:false,inList:['SuperFantasticAndAwesome','SuperBadAndUltraBoring'])
}
}
So your Author class becomes:
class Author {
...
static hasMany = [authorBooks:AuthorBook]
}
In this way the status is stored as a value of the join and statuses can be added, updated, or removed as needed in the future. It does have the side effect of having to query through the AuthorBook class to get to the associated Books.
See also: http://grails.org/Many-to-Many+Mapping+without+Hibernate+XML
I just ended up using 2 join tables:
static mapping = {
superFantasticAndAwesomeBooks joinTable: [name: 'awesomeBooks', key: 'book_id']
superBadAndUltraBoringBooks joinTable: [name: 'boringBooks', key: 'book_id']
}
Using ArgoUML, I very quickly created this trivial representation of a few Domain classes (Person, Store, Product) and their relationships.
I'm struggling with the implementation of the relationships. Below was my initial approach for the Person domain, but it seems that I am missing something important.
class PersonToPerson {
Person from
Person to
String relation
static constraints = {
relation(inList:["Friend to", "Enemy of", "Likes", "Hates"])
}
static belongsTo = [ Person ]
}
class Person {
String firstName
String secondName
.
.
.
static hasMany= [ personToPerson:PersonToPerson,
personToStore:PersonToStore ]
}
Edit: updated question for clarity
After thinking on the problem I think I have a better way to ask the question(s). In the implementation of PersonToPerson above I have the relation as a simple string. I want the user to be able to select from a list of unique relations, which are defined in the constraints, for the string value for PersonToPerson. So this leads to the questions...
Should personToPerson and personToStore be consolidated into one list of type Relationship? Or should they stay independent lists as shown?
What is the mechanism to allow the user to add new values to the relation constraint?
1) Domain model
Keep your code simple. Don't create generic data model. It's way to hell. When you personToPerson and personToStore keep separate, it's much easier to follow your code.
Actually suggested solution makes it possible to access relations as consolidated and independent list simultaneously.
For this problem I would use inheritance feature in GORM.
Your classes would look like this:
class Person {
String name
static hasMany = [personToPerson:PersonToPerson,
personToProduct:PersonToProduct,
personToStore:PersonToStore]
static mappedBy = [personToPerson:"from"]
}
class Product{
String productName
}
class Relationship{
String note
}
class Store{
String storeName
}
class PersonToPerson extends Relationship{
Person from
Person to
String relation
static constraints = {
relation(inList:["Friend to", "Enemy of", "Likes", "Hates"])
}
static belongsTo = [ from:Person ]
}
class PersonToProduct extends Relationship{
Person person
Product product
String relation
static constraints = {
relation(inList:["likes", "dislikes"])
}
static belongsTo = [ person:Person ]
}
class PersonToStore extends Relationship{
Person person
Store store
String relation
static constraints = {
relation(inList:["Stock person", "Owner", "Manager", "Patron"])
}
static belongsTo = [ person:Person ]
}
DB schema for Person, Product and Store is usual. But for Relational domains look like this:
Relationship
Field Type Null Default
id bigint(20) No
version bigint(20) No
note varchar(255) No
class varchar(255) No
person_id bigint(20) Yes NULL
product_id bigint(20) Yes NULL
relation varchar(8) Yes NULL
from_id bigint(20) Yes NULL
to_id bigint(20) Yes NULL
store_id bigint(20) Yes NULL
Relationship domain makes possible to access all relational domain throw one domain.
2) Constraint
Just switch inList to validator. Than you can store constrain in file or DB.
See documentation or file example.
Example how to store constraint values in DB. First create a domain object.
class Constrain{
String name
String type
}
Than the domain class looks:
class PersonToPerson extends Relationship{
Person from
Person to
String relation
static constraints = {
relation(nullable:false, validator:{val, obj ->
def list = Constrain.findAllByType('person').collect{it.name}
return list.contains(val)
})
}
static belongsTo = [ from:Person ]
}
Look fine. You may want to consider a belongsTo in the PersonToPerson class.
Also, your has many in Person should be: [ personToPersons:PersonToPerson.... <- remove the s