-
Notifications
You must be signed in to change notification settings - Fork 75
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
EqualsVerifier should check for field usage #650
Comments
Hi! Thanks for opening this issue. I'm not too familiar with Hibernate, so I don't completely understand what you're asking for. Can you explain to me how these lazy-loaded objects work? And how it can be that they can equal each other but not identical non-lazy objects? Is the equals method you posted from a lazy or a non-lazy entity? The more information you give me the better, I have to work on this on my spare time and I don't have a lot of it. Every minute spent researching is a minute spent not coding :) |
@jqno Sorry for the late reply. Didn't notice the notification. To be precise: It is more an issue with Hibernate in combination with JPA. Given a simple Book table, the corresponding model in Java would look like this: class Book {
private int id;
private String isbn;
private String title;
public String getIsbn() {
return this.isbn;
}
public String getTitle() {
return this.title;
}
public boolean equals(Object o) {
...
return Object.equals(this.isbn, other.isbn) && Object.equals(this.title, other.isbn);
}
public boolean equals2(Object o) {
...
return Object.equals(this.getIsbn(), other.getIsbn()) && Object.equals(this.getTitle(), other.getIsbn());
}
} Please note the Additionally, we have a JPA repository interface that looks like this: @Repository
interface BookRepository extends CrudRepository<BookModel, Integer> {
// This will be implemented by JPA/Hibernate. The corresponding query would be something like this:
// SELECT book FROM BookModel book WHERE book.isbn = ?1 AND book.title = ?2
BookModel findBookByIsbnAndTitle(String isbn, String title);
} This interface will be implemented by JPA and used in further snippets by the variable name Now, given this simple model, we can assume the following two lines to be true:
The main problem however is the lazy-loading feature. public String getTitle() {
if (!this.isLoaded()) {
// Load the original model by id and publish the fields locally
this.loadPropertiesFromDb();
this.setLoaded(true);
}
return super.getTitle();
} In terms of memory representation, the object fetched by Hibernate would look like this:
while the locally created instance would look like this
The important bit about this information is, that the book fetched by Hibernate references a that does equal the locally created book. However, until the first call to any getter, the information will not be represented within the objects' fields. Objects.equals(this.isbn, other.isbn) && Objects.equals(this.title, other.title);
// Results in following test:
Objects.equals(null, "isbn") && Objects.equals(null, "title"); While on the other hand, Objects.equals(this.getIsbn(), other.getIsbn()) && Objects.equals(this.getTitle(), other.getTitle());
// First call to getIsbn() causes Hibernate to load the actual data.
// Therefore, the comparison would look like this:
Objects.equals("isbn", "isbn") && Objects.equals("title", "title"); I hope this explanation of the process is understandable, otherwise please feel free to contact me with any further questions. |
The simplest approach at testing this problem dynamically would be something like this: BookModel instance = new BookModel("isbn", "title");
class DynamicSubClass extends BookModel {
private BookModel _delegate;
public String getIsbn() {
return this._delegate.getIsbn();
}
public String getTitle() {
return this._delegate.getTitle();
}
}
final var DynamicSubClass lazyObject = new DynamicSubClass();
lazyObject.set_delegate(bookModel);
assertEquals(lazyObject, bookModel); If the test fails, at least some of the equals method uses fields directly. |
Thanks for the clarification! I think I understand what you mean. So if I understand you correctly, the issue is not that EqualsVerifier currently gives incorrect answers to a class that you give it. The issue is that sometimes at runtime, an object may not be fully loaded yet, and if the equals method was implemented using fields, the results will sometimes be incorrect because the fields are still null. The correct way to write an equals method when dealing with lazy loading, is that you use the getters instead of the fields, and what you're asking is that we add a check to see if this is actually how the equals method is implemented, am I correct? So the new check will fail if equals is implemented using fields, and succeed when equals is implemented using getters. What EqualsVerifier needs to do then, is 1) detect if a field is loaded lazily or eagerly, and 2) if lazy, check that the getter is used instead of the field itself. Regarding 1, I've been reading up a bit, and I understand that eager loading is the default, and you can define lazy loading using an annotation on the field: Regarding 2, I'm not 100% sure yet if this is even feasible (bytecode manipulation is really hard and so far I've been able to avoid doing it myself). Looks like a cool challenge, but one that will take a while to complete 😅 |
Yes, the equals verifier behaves precisely as I'd expect it to; this is just intended as an extension. Regarding 1, there are multiple annotations to define lazy loading.
The list is compiled based on what I've used before and the content of the The main problem I see on this front would be the ways Hibernate accepts configuration. This might be entirely biased, but I'd say we should focus on the annotations first and maybe add something like Regarding 2, According to this Baeldung article Hibernate uses a dynamic proxy. My approach would be to emulate lazy loaded fields instead of detecting getter usage over direct property access. Small side note: |
POC pushed here Notes:
|
Thanks once again for the elaborate reply and the PoC 🙂 Re 1, that's a lot of annotations, but that shouldn't be a problem. Is it true that on 6 each case, the thing to look for is Re 2, since EqualsVerifier already uses ByteBuddy, I figure it's best to use that. Saves a dependency... I'm still thinking of ways to keep things as simple as possible though. Maybe EqualsVerifier can generate for each lazy property an overridden getter that throws a specific exception, and then it should fail of the exception isn't thrown. It's not pretty but it's the least complicated thing I can think of right now... |
I've just released EqualsVerifier 3.12, which checks that getters are used for lazy-loaded fields. Let me know if it works for you! |
Is your feature request related to a problem? Please describe.
Hibernate does sometimes initialize objects lazily via a proxy.
It can be nice; however, the comparison will use invalid data if the equals method accesses the fields directly rather than using getters.
The result will always be false if we compare a lazy-loaded object against an eagerly loaded object.
If we compare two lazy-loaded objects against each other, the result is presumably always true.
Describe the solution you'd like
It would be nice to have a test as follows:
Describe alternatives you've considered
An alternative would be creating a subclass and verifying that the getters are called.
Additional context
Here a sample equals-method to demonstrate the problem:
The text was updated successfully, but these errors were encountered: