Say I have the following (heavily simplified) GORM domain classes:
class PhoneCall extends Interaction {
Survey survey
}
class Survey {
String campaignCode
Integer clientId
Boolean isDynamic
List interactions
static constraints = {
campaignCode unique: true, nullable: false
clientId nullable: true
isDynamic nullable: true
}
static hasMany = [interactions: Interaction]
}
class Interaction {
String clazz
Instant dateCreated
static constraints = {
}
static mapping = {
tablePerHierarchy false
autoTimestamp false
}
def beforeInsert() {
dateCreated = Instant.now()
}
}
I have the following simple code to set up these classes for a test:
def survey = new Survey(campaignCode: "TEST", isDynamic: true).save(failOnError: true, flush: true)
def phoneCall = new PhoneCall(survey: survey, clazz: PhoneCall.name).save(failOnError: true)
This fails with the following stack trace:
org.springframework.dao.DataIntegrityViolationException: could not insert: [uk.co.nttfundraising.onitfhi.domain.PhoneCall]; SQL [insert into phone_call (id) values (?)]; constraint [survey_id]; nested exception is org.hibernate.exception.ConstraintViolationException: could not insert: [uk.co.nttfundraising.onitfhi.domain.PhoneCall]
at org.springframework.orm.hibernate3.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:643)
at org.springframework.orm.hibernate3.HibernateAccessor.convertHibernateAccessException(HibernateAccessor.java:412)
at org.springframework.orm.hibernate3.HibernateTemplate.doExecute(HibernateTemplate.java:412)
at org.springframework.orm.hibernate3.HibernateTemplate.execute(HibernateTemplate.java:339)
at org.codehaus.groovy.grails.orm.hibernate.metaclass.SavePersistentMethod.performSave(SavePersistentMethod.java:56)
at org.codehaus.groovy.grails.orm.hibernate.metaclass.AbstractSavePersistentMethod.doInvokeInternal(AbstractSavePersistentMethod.java:215)
at org.codehaus.groovy.grails.orm.hibernate.metaclass.AbstractDynamicPersistentMethod.invoke(AbstractDynamicPersistentMethod.java:63)
at org.codehaus.groovy.grails.orm.hibernate.HibernateGormInstanceApi.save(HibernateGormInstanceApi.groovy:196)
However, if I remove the line List interactions from Survey (making interactions into a Set), everything works fine. There are also no problems if I use SortedSet interactions, though the generated database schema doesn't seem to have any notion of order so I'm unsure about that solution. Google mostly suggests not saving the Survey (e.g. this blog post) but I've tried this to no avail.
It's only the List that fails, and it causes the insert into PhoneCall to completely ignore my Survey! What's going on?
A caveat to using a List is that the item you add to it cannot be save()d prior to adding to the List. But more importantly, the proper way to add items to a collection when using a one-to-many association is to use survey.addToInteractions(), See addTo*(). But first, you need a proper association...
class PhoneCall extends Interaction {
static belongsTo = [survey: Survey]
}
By replacing the Survey property with belongsTo, you get a proper bi-directional one-to-many association. Then, you can use/test it like this:
def survey = new Survey(campaignCode: "TEST", isDynamic: true)
survey.addToInteractions(new PhoneCall(survey: survey, clazz: PhoneCall.name))
survey.save(failOnError: true, flush: true)
Notice that the PhoneCall is never explicitly saved, and PhoneCall.survey is not explicitly assigned. All of this gets taken care of when survey.save() is called.
Once saved, someSurvey.interactions[index].survey will reference the someSurvey.
Related
Sample project: https://github.com/joemccall86/cascade-delete-test/tree/automatic-collection-purge
I'm not sure if this is a bug since I haven't seen an example of it working anywhere.
Say I have a the following domain classes:
class Organization {
String name
static hasMany = [
users: Person,
teams: Team
]
static mapping = {
users cascade: 'all-delete-orphan'
}
}
class Person {
String name
Organization organization
static belongsTo = [Organization, Team]
static hasMany = [teams: Team]
static constraints = {
}
def beforeDelete() {
Team.withNewSession {
removeFromAllTeams()
}
true
}
def removeFromAllTeams() {
Team.where {
members {
id == this.id
}
}.each { Team team ->
if (team.members.contains(this)) {
team.members.remove(this)
team.save()
}
}
}
}
class Team {
String name
static hasMany = [members: Person]
static belongsTo = [organization: Organization]
static constraints = {
}
}
According to https://spring.io/blog/2010/07/02/gorm-gotchas-part-2/ (specifically the section regarding many-to-many relationships cascading), I need to manually clear the persons from the team before I can perform a successful deletion. That is done with the method removeFromAllTeams.
I can call that manually every time I plan on deleting a user (which would typically happen inside a service call), but it looks more like it belongs inside a beforeDelete method on the domain object itself. However when I put it there, I get:
SQL [n/a]; Referential integrity constraint violation: "FKGHKKY8WMH379RPMFH92T807RY: PUBLIC.TEAM_MEMBERS FOREIGN KEY(PERSON_ID) REFERENCES PUBLIC.PERSON(ID) (1)"; SQL statement:
delete from person where id=? and version=? [23503-194]; nested exception is org.h2.jdbc.JdbcSQLException: Referential integrity constraint violation: "FKGHKKY8WMH379RPMFH92T807RY: PUBLIC.TEAM_MEMBERS FOREIGN KEY(PERSON_ID) REFERENCES PUBLIC.PERSON(ID) (1)"; SQL statement:
delete from person where id=? and version=? [23503-194]
Here's a spock test to illustrate what I'm doing:
#Unroll
void "user is removed from team before deletion, runManually = #runManually"() {
given: 'an existing org'
Organization.withNewSession {
def organization = new Organization(name: 'Cyberdyne Systems').save(failOnError: true)
and: 'a person is added'
organization.addToUsers(name: 'John Connor').save(failOnError: true)
organization.save(failOnError: true, flush: true)
and: 'a new team is added to the org'
organization.addToTeams(name: 'IT').save(failOnError: true, flush: true)
and: 'the person is added to the team'
organization.teams.first().addToMembers(organization.users.first())
organization.save(failOnError: true, flush: true)
}
and: 'the person is deleted'
def userToDelete = Person.first()
if (runManually) {
userToDelete.removeFromAllTeams()
}
userToDelete.delete(flush: true)
expect: 'the team has no users'
Team.first().members.isEmpty()
and: 'there are no more users'
Person.count == 0
where:
runManually << [true, false]
}
What am I missing? In other words, is it even possible to have a domain class do its own relationship clean-up inside beforeDelete?
I have experienced this issue with Grails 3.2.12 and GORM 6.1.6.RELEASE.
Like you, I tried adding the clean-up code to the beforeDelete method, but it didn't work. According to my commit log:
"For some reason, this cannot be done in beforeDelete, possibly because GORM checks whether the delete is valid before calling beforeDelete".
I suggest you create an issue on GitHub and ask the Grails team to clarify whether this is a bug or a feature.
I want to add a child to two different parent class, like that:
First:
class Member {
Profile profile
Member() {
this.profile = new Profile()
}
static mapping = {
profile cascade: 'all-delete-orphan'
}
}
Second:
class Team {
Profile profile
Team() {
this.profile = new Profile()
}
static mapping = {
profile cascade: 'all-delete-orphan'
}
}
Thw child is simply define like that
class Profile() {
}
The probleme is when I save the parent, it dosent save the child:
Member member = new Member().save(flush: true, failOnError: true)
assert !member.hasErrors()
assert member.profile
assert !member.profile.hasErrors()
assert member.profile.id //FAIL
What do I do wrong? Is there a better way to do it?
UPDATE:
I found this
Saving associated domain classes in Grails
It seem that 'belong to' is needed for that kind of behavior. But Why the 'cascade: 'all-delete-orphan' doesn't force this ? Because I can't use 'belong to' in that specific case
I copied your example (with slight modification to change group table name to a non-reserved word) and the cascades are working properly using grails 2.2.1. Both Member and Group cascaded their saves to the newly created Profiles.
Assuming your classes are more complicated than this, you might have an error elsewhere in your class (eg cascade behavior described in constraints instead of mapping, etc).
I found a nice solution. The best was to define both parent in the child but nullable. Like that:
class Profile() {
static belongsTo = [member: Member, team: Team]
static constraints = {
member nullable: true
team nullable: true
}
}
This way, the cascade behavior work just fine !
I have one-to-many relationship between entities - RentalUnit and Review . All tests run fine, except when i am adding the same review to different RentalUnit instances as following:
def review3 = Review.build().save(flush: true)
def rentalUnit2 = RentalUnit.build().
addToReviews(review2).addToReviews(review3).save(flush: true)
assert rentalUnit.reviews.contains(review2)
It seems that GORM and the addTo* method doesn't seem to care if i am adding the same Review instance, so i am guessing there is something missing in my domain classes. What would that be?
Thank you for helping
p.s.
class Review {
String submittedBy
String content
String dateReceived
boolean isApproved
static belongsTo = RentalUnit
static mapping = {
content type: 'text'
}
static constraints = {
submittedBy blank: false, size: 3..50
content blank: false, size: 5..2500
}
}
class RentalUnit {
String name
String nickname
Address address
static hasMany = [reviews:Review]
static mapping = {
reviews cascade: "all-delete-orphan"
}
static constraints = {
name blank: false, unique: true, size: 4..10
nickname blank: false, size: 5..60
}
}
Yes it doesn't care - it doesn't change any other objects' properties. Even about the other end of relationship - review2.rentalUnit (if such field existed) will be null here.
The Review will disappear from rentalUnit.reviews (or review2.rentalUnit would be assigned) the next time the object is loaded from database.
You can assign review2.rentalUnit by hand and validate if it's been added to another RentalUnit, though - the assignment will do no harm.
edit: let's go through the code step-by-step.
rentalUnit1.addToReviews(review2)
Here review2 is added to rentalUnit1.reviews. review2.rentalUnit is NOT assigned, but after save in the database it would point to rentalUnit1. The only persistent representation of RentalUnit.reviews field is a child-to-parent reference field, Review.rentalUnit.
def rentalUnit2 = ...
rentalUnit2.addToReviews(review2).addToReviews(review3).save(flush: true)
Here review2 is added to rentalUnit2.reviews. review2.rentalUnit is NOT assigned again. review2 is NOT removed from rentalUnit1.reviews BUT, after save, in the database, it will point to rentalUnit2.
assert rentalUnit1.reviews.contains(review2)
review2 was NOT removed from rentalUnit1.reviews, so the assertion will pass. BUT in the next session rentalUnit1 and rentalUnit2 will have the correct sets of reviews - only rentalUnit2 will have review2.
Now, if you want to always maintain Java representation consistent, implment a method like this:
class RentalUnit {
RentalUnit addToReviewsAndCheck(Review r) {
if (r.rentalUnit == this) return;
if (r.rentalUnit != null) {
r.rentalUnit.removeFromReviews(r)
}
r.rentalUnit = this
addToReviews(r)
}
}
but it's an overkill, as for me.
I have the following domain classes:
Holiday:
class Holiday {
String justification
User user
//static belongsTo = User
static constraints = {
}
}
User:
class User {
String login
String password
static hasMany = [ holidays : Holiday ]
static constraints = {
}
}
I have created a one-many relationships between Holiday and User. Note that I haven't included belongsTo on the Holiday class. Now I have written the following integration test:
void testWithoutBelongsTo() {
def user1 = new User(login:"anto", password:"secret")
user1.save()
def holiday1 = new Holiday(justification:"went to trip")
holiday1.save()
user1.addToHolidays(holiday1)
assertEquals 1, User.get(user1.id).holidays.size()
user1.delete()
assertFalse User.exists(user1.id)
assertFalse Holiday.exists(holiday1.id)
}
Clearly in the above test case, I'm deleting only the user1 instance, but when I run with assert statements I can see that GORM have implicitly deleted holiday1, too. And my Test cases have PASSED! How this can happen, even though I haven't given belongsTo keyword in Holiday class?
I'm using Grails version 1.3.7.
holiday1 was never saved as it does not validate: property user is neither set nor nullable.
Here is how your code in Holiday.groovy should look like:
class Holiday {
String justification
User user
//static belongsTo = User
static constraints = {
user(nullable: true)
}
}
And your test, with property user set properly in holiday1:
void testWithoutBelongsTo()
{
def user1 = new User(login:"anto", password:"secret")
user1.save(failOnError: true)
def holiday1 = new Holiday(justification:"went to trip",
user: user1) // Set user properly
holiday1.save(failOnError: true)
user1.addToHolidays(holiday1)
assert 1, User.get(user1.id).holidays.size()
holiday1.user = null // Unset user as otherwise your DB
// won't be happy (foreign key missing)
user1.delete()
assert ! User.exists(user1.id)
assert Holiday.exists(holiday1.id)
}
In order to eliminate validation errors quickly in your tests, always use save(failOnError: true). It will throw an exception if your object do not validate.
You should save holiday after adding it to the user.
I have a User domain class, and a List one.
Each list must have an author (a user) and each user must have a "primary list". Only some of the lists will have the "primaryList" statute.
So.. somthing like
User:
List primaryList
List:
User author
static belongsTo = User
Of course this does not work as intended because the two relations are mistakenly taken as only one. I should also add a hasMany on the User and other belongsTo to the List..., but I don't want to complicate the example because I want to get the right answer from you.
You may need to use mappedBy to explain how the fields in User and List line up. Here are a couple domains that I wrote that allow a User to author many Lists but only set one to be "primary". There are a couple extra nullable constraints so you can use the scaffolded UI without getting into a chicken-and-egg scenario.
class User {
String name
FooList primaryList
static hasMany = [authoredLists: FooList]
static mappedBy = [primaryList: 'primaryOwner', authoredLists: 'author']
static constraints = {
primaryList nullable: true, unique: true
authoredLists nullable: true
}
String toString() { name }
}
I named this class "FooList" just to avoid confusion with the standard List class:
class FooList {
static belongsTo = [author: User, primaryOwner: User]
static constraints = {
primaryOwner nullable: true, display: false
}
}
Try using the map belongsTo approach:
static belongsTo = [user:User]
That way Grails should see the 2 properties as separate.