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
JAVA-3567: Add support for records #581
Conversation
try { | ||
getter = clazz.getDeclaredMethod(name); | ||
} catch (NoSuchMethodException e) { | ||
throw new AssertionError("Record must have getter method with same name as field"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't sure what to put in the catch block. These calls should never fail because the methods are guaranteed to to exist with records.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally we seem to use a CodecConfiguration exception for pretty much all exceptions in Codecs. I think an IllegalStateException would be fine here and below. Also pass the NoSuchMethodException exception on as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, done.
/** | ||
* A Builder for the RecordCodecProvider | ||
*/ | ||
public static final class Builder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class unfortunately contains a lot of duplicate code with the Builder in PojoCodecProvider. I considered making an abstract builder, but that would require me to either make the instance variables protected (which the style checker complained about) or add getter methods for all of the instance variables. I figured the extra code that was required to make that work wasn't worth it, but let me know if that was a mistake.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also may want to consider making an abstract UserDefinedCodecProvider, because RecordCodecProvider and PojoCodecProvider are so similar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with your decision to copy the code
@@ -79,8 +79,8 @@ configure(coreProjects) { | |||
configure(javaProjects) { | |||
apply plugin: 'java-library' | |||
|
|||
sourceCompatibility = JavaVersion.VERSION_1_8 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can ignore all this, we'll have to delete it before merging anyway.
import static org.bson.codecs.configuration.CodecRegistries.fromProviders; | ||
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries; | ||
|
||
public class RecordTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is very ugly because it won't be merged. You can ignore the test logic, it just implements round trip testing and go straight to the tests if you want to look at them (but like I said those tests aren't comprehensive).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also I had to put this stuff in a new module because I couldn't get the bson module to run with Java 14 in preview mode. However, the bson module can still access the 14 preview features, if we run the code from a different module.
I think this will be ok. We will have to compile all the annotations with Java 16, but still target Java 8. This isn't so different with what we do now: compile with Java 11 but target Java 8. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks really good, I had a couple of comments
d46d57f
to
1ca9c6a
Compare
properties.add((BsonProperty) annotation); | ||
break; | ||
} | ||
static Integer initializeDataFromAnnotations(Annotation[][] parameterAnnotations, List<String> properties) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple things strike me as a little sloppy in this method...
- Passing an empty list in, just so things can be added to it
- Doing two things at once (initializing properties list and returning the id index)
If I didn't have this method, the code would need to be duplicated in both Constructor and MethodCreatorExecutable. If I broke it into two separate functions it would require two passes over the annotations list.
static <T> void configureClassModelBuilder(final ClassModelBuilder<T> classModelBuilder, final Class<T> clazz) { | ||
classModelBuilder.type(notNull("clazz", clazz)); | ||
|
||
if (isRecord(clazz)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jyemin in the design doc you suggested "inverting the dependency" so that whoever constructed the ClassModelBuilder was responsible for configuring it correctly (i.e. RecordCodecProvider calls configureRecordClassModelBuilder). I implemented this initially, but I realized this would introduce a breaking change.
ClassModel::builder is part of the public API and it constructs a ClassModelBuilder which originally called the configureClassModelBuilder method. To invert the dependency we would get rid of this call. Previously users get a fully configured classModelBuilder just by calling ClassModel#builder, now it isn't configured it all. This is bad because we accept ClassModels in our PojoCodecProvider in one of the register functions. I believe this means we can't invert the dependency so I did it this way instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good - couple of minor nits.
bson/src/main/org/bson/codecs/pojo/MethodCreatorExecutable.java
Outdated
Show resolved
Hide resolved
try { | ||
getter = clazz.getDeclaredMethod(name); | ||
} catch (NoSuchMethodException e) { | ||
throw new AssertionError("Record must have getter method with same name as field"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally we seem to use a CodecConfiguration exception for pretty much all exceptions in Codecs. I think an IllegalStateException would be fine here and below. Also pass the NoSuchMethodException exception on as well.
@@ -55,6 +55,7 @@ | |||
private String discriminator; | |||
private String discriminatorKey; | |||
private String idPropertyName; | |||
private boolean isRecord; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this to avoid a call to PojoBuilderHelper::isRecord
in ConventionAnnotationImpl
. It's not added to the ClassModel
that is built.
|
||
classModelBuilder.instanceCreatorFactory(new InstanceCreatorFactoryImpl<T>(new CreatorExecutable<T>(clazz, noArgsConstructor))); | ||
for (Annotation a : recordComponent.getAnnotations()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because records are immutable, they won't have setters, so we can't really have the read/write breakdown we use for annotations in Pojos. Therefore I just add all the annotations to both the read and write annotations.
Also, by implementing it this way, I am requiring users to put the annotation on the RecordComponent (as opposed to a custom getter if they implement one). This seems reasonable to me, as overriding the getter (or writing a new one) should be pretty rare and I can't think of a reason a user would want to annotate the getter instead of the record component.
@@ -633,7 +633,7 @@ public void testRoundTripWithoutBsonAnnotation() { | |||
return conventions; | |||
} | |||
|
|||
class ObjectCodec implements Codec<Object> { | |||
static class ObjectCodec implements Codec<Object> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Necessary in order to reuse ObjectCodec in my RecordCustomTest.
encodesTo(getRecordCodecRegistry(BsonRepresentationUnsupportedIntRecord.class), new BsonRepresentationRecord("notanobjectid", 1), null); | ||
} | ||
|
||
private static <T> RecordCodecProvider getRecordProviderFromClassModel(ClassModelBuilder<T> classModelBuilder) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These helpers are in this class as opposed to PojoTestCase because I wanted to keep all record related code out of PojoTestCase in case the record test code needs to go in a separate module. The way I organized it is helpers specific to RecordCustomTest go in RecordCustomTest, helpers specific to RecordRoundTripTest go in RecordRoundTripTest, and tests shared by both of them go in RecordRoundTrip and are package private.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's looking great, and the tests are very thorough. Just a few comments.
Also: so that we don't forget, can you also add the RecordCodecProvider to the default registriy in MongoClientSettings?
/** | ||
* A Builder for the RecordCodecProvider | ||
*/ | ||
public static final class Builder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with your decision to copy the code
static <T> void configureClassModelBuilder(final ClassModelBuilder<T> classModelBuilder, final Class<T> clazz) { | ||
classModelBuilder.type(notNull("clazz", clazz)); | ||
|
||
if (isRecord(clazz)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
bson/src/main/org/bson/codecs/pojo/RecordCreatorExecutable.java
Outdated
Show resolved
Hide resolved
d6a4f6d
to
1fc710c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While reviewing the last commit I found a few more nits for you to consider.
bson/src/main/org/bson/codecs/pojo/RecordCreatorExecutable.java
Outdated
Show resolved
Hide resolved
bson/src/main/org/bson/codecs/pojo/RecordCreatorExecutable.java
Outdated
Show resolved
Hide resolved
bson/src/main/org/bson/codecs/pojo/RecordCreatorExecutable.java
Outdated
Show resolved
Hide resolved
I think this looks great - I'm looking forward to it landing in master! Great work on migrating all the pojo test models over to records - I forgot how many there are! And for testing the record / pojo interop. |
Also LGTM! Great work! |
This branch has been pushed to the main repo and will be merged once Java 16 is released. |
@bdeleonardis1 Any news now that it's been released for a while? |
@cardouken thanks for your interest. This is definitely on our radar to merge, but it turns out there is more work to be done on the build side before it's ready to go. IIRC we have to upgrade Gradle in order to build with Java 16, and that upgrade is not straightforward. |
@jyemin thanks for the quick update, looking forward to it coming out. |
Any news on this now? |
This is unblocked. The driver has been upgraded to Gradle 7.3 and builds with Java 17. So we're going to pick this back up soon. |
@jyemin thanks for the several updates. Do you have any further information about the possibility of this moving forward? |
We are still planning to move forward, but encountered a few more build complications. Apologies for the further delays. |
Thank you for the update! |
Any further news on this? |
See JAVA-3567. It was released in 4.6.0. |
I see, thanks! |
Leaving as a draft for the following reasons:
I think it makes the most sense to hold off on fixing these problems until records are generally available instead of preview mode because it will save a lot of time and configuration headaches.