Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relationships OneToOne using proxies are incorrectly marked as dirty #1294

Closed
mvinas1977 opened this issue Apr 17, 2020 · 2 comments
Closed
Assignees
Milestone

Comments

@mvinas1977
Copy link

mvinas1977 commented Apr 17, 2020

Let’s take a look the following example. We have these domain objects

@Entity
class TestAuthor {
    String name
    long version
}

@Entity
class TestBook {
    String title
    long version
    TestAuthor author
}

So, you get a book from db

TestBook book = TestBook.get(someId)

At this Point book.author is a proxy, that is ok. If during binding author is set with same value it previously had it will we marked as dirty

No Mather if you set the proxy

book.author = book.author

Or a new copy of the same Author

book.author = TestBook.get(book.author.id)

In both cases author is marked as dirty

Expected Behaviour

Since book is same instance author property should not marked as dirty

Actual Behaviour

Author is incorrectly marked as dirty

Updated DirtyCheckingSpec to reproduce the issue

package org.grails.datastore.gorm

import grails.gorm.annotation.Entity
import grails.gorm.tests.GormDatastoreSpec

import grails.gorm.tests.Person
import org.grails.datastore.mapping.dirty.checking.DirtyCheckable
import org.grails.datastore.mapping.proxy.EntityProxy

/**
 * @author Graeme Rocher
 */
class DirtyCheckingSpec extends GormDatastoreSpec {

    @java.lang.Override
    List getDomainClasses() {
        return [TestAuthor, TestBook]
    }

    void "Test that dirty checking methods work when changing entities"() {
        when:"A new instance is created"
            def p = new Person(firstName: "Homer", lastName: "Simpson")

        then:"The instance is dirty by default"
            p instanceof DirtyCheckable
            p.isDirty()
            p.isDirty("firstName")

        when:"The instance is saved"
            p.save(flush:true)

        then:"The instance is no longer dirty"
            !p.isDirty()
            !p.isDirty("firstName")

        when:"The instance is changed"
            p.firstName = "Bart"

        then:"The instance is now dirty"
            p.isDirty()
            p.isDirty("firstName")
            p.dirtyPropertyNames == ['firstName']
            p.getPersistentValue('firstName') == "Homer"

        when:"The instance is loaded from the db"
            p.save(flush:true)
            session.clear()
            p = Person.get(p.id)

        then:"The instance is not dirty"
            !p.isDirty()
            !p.isDirty('firstName')

        when:"The instance is changed"
            p.firstName = "Lisa"

        then:"The instance is dirty"
            p.isDirty()
            p.isDirty("firstName")
    }

    void "test relationships not marked dirty when proxies are used"() {
        given:
        Long id
        Long authorId

        TestBook.withNewTransaction {
            TestBook.withSession {
                TestAuthor author = new TestAuthor(name: 'Jose Hernandez')
                TestBook book = new TestBook(title: 'Martin Fierro', author: author)
                book.save(flush: true, failOnError: true)
                id = book.id
                authorId = author.id
            }
        }

        when:
        TestAuthor author
        TestBook book1

        TestBook.withNewTransaction {
            TestBook.withNewSession {
                book1 = TestBook.get(id)
                author = book1.author
                book1.author = author
            }
        }

        then:
        isProxy(author)
        !book1.isDirty('author')
        !book1.isDirty()

        cleanup:
        TestBook.withNewTransaction {
            TestBook.withSession {
                TestAuthor.get(authorId)?.delete()
                TestBook.get(id)?.delete()
            }
        }
    }

    void "test relationships not marked dirty when domain objects are used"() {
        given:
        Long id
        Long authorId

        TestBook.withNewTransaction {
            TestBook.withSession {
                TestAuthor author = new TestAuthor(name: 'Jose Hernandez')
                TestBook book = new TestBook(title: 'Martin Fierro', author: author)
                book.save(flush: true, failOnError: true)
                id = book.id
                authorId = author.id
            }
        }

        when:
        TestAuthor author
        TestBook book1

        TestBook.withNewTransaction {
            TestBook.withNewSession {
                book1 = TestBook.get(id)
                author = TestAuthor.get(authorId)
                book1.author = author

            }
        }

        then:
        !isProxy(author)
        !book1.isDirty('author')
        !book1.isDirty()

        cleanup:
        TestBook.withNewTransaction {
            TestBook.withSession {
                TestAuthor.get(authorId)?.delete()
                TestBook.get(id)?.delete()
            }
        }
    }

    private boolean isProxy(def item) {
        return item instanceof EntityProxy
    }
}

@Entity
class TestAuthor {
    String name
    long version

}

@Entity
class TestBook {
    String title
    long version
    TestAuthor author
}

This issue is reproducible in 6.1.x 7.x and master

#1296 PR with fix for 6.1
#1298 PR with fix fot 7.x
#1297 PR with fix for master

This was referenced Apr 17, 2020
@puneetbehl puneetbehl self-assigned this Apr 27, 2020
@puneetbehl puneetbehl added this to the 7.0.5 milestone Apr 27, 2020
@puneetbehl
Copy link
Contributor

@mvinas1977 I think all tests would pass when you define equals and hashCode for TestAuthor or annotate the class with @EqualsAndHashCode.

@puneetbehl
Copy link
Contributor

puneetbehl commented Apr 30, 2020

A better solution would be to Override equals method because unwrapping and calling EqualsAndHashcode would make the actual query to the database. So, the following implementation should solve the problem:

@Entity
class TestAuthor {
    String name
    long version

    @Override
    boolean equals(o) {
        if (!(o instanceof TestAuthor)) return false
        if (this.is(o)) return true
        TestAuthor that = (TestAuthor) o
        if (id !=null && that.id !=null) return id == that.id
        return false
    }
}

puneetbehl added a commit that referenced this issue Apr 30, 2020
to demonstrate the need to override equals method for correctly marking association as dirty or not.
puneetbehl added a commit that referenced this issue Apr 30, 2020
* Cleanup and remove redundant code.
* Update implementation of markDirty to consider EntityProxy and update the comparison operation accordingly.
puneetbehl added a commit to grails/gorm-hibernate5 that referenced this issue May 8, 2020
puneetbehl added a commit to grails/gorm-mongodb that referenced this issue May 8, 2020
puneetbehl added a commit to grails/gorm-hibernate5 that referenced this issue May 8, 2020
puneetbehl added a commit to grails/gorm-mongodb that referenced this issue May 8, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants