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

JAVA-3567: Add support for records #581

Closed
wants to merge 1 commit into from

Conversation

bdeleonardis1
Copy link
Contributor

@bdeleonardis1 bdeleonardis1 commented Sep 24, 2020

Leaving as a draft for the following reasons:

  1. Testing is not finished. I was unable to get Junit running successfully in preview mode so I wrote my own little test runner. The tests I wrote make me confident that my implementation is correct, but we're going to want to add more coverage before merging this. All of the tests I have written so far are integration tests. I did not write a full suite of tests because I didn't want to waste time writing them in one format, when they'll just need to be converted to Junit tests before this gets merged anyway.
  2. Annotations are not supported. I was having a problem getting the ElementType.RecordComponent to work because of preview mode. If we are able to use ElementType.RecordComponent it will be very easy to support annotations, but if this causes a Java 8 compatibility problem we'll have to think harder about how to do that.
  3. There is a bunch of junk in this diff that I needed to do to get preview mode to work that needs to be deleted before it is merged.

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.

try {
getter = clazz.getDeclaredMethod(name);
} catch (NoSuchMethodException e) {
throw new AssertionError("Record must have getter method with same name as field");
Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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 {
Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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).

Copy link
Contributor Author

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.

@jyemin
Copy link
Contributor

jyemin commented Sep 24, 2020

If we are able to use ElementType.RecordComponent it will be very easy to support annotations, but if this causes a Java 8 compatibility problem we'll have to think harder about how to do that.

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.

Copy link
Member

@rozza rozza left a 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

properties.add((BsonProperty) annotation);
break;
}
static Integer initializeDataFromAnnotations(Annotation[][] parameterAnnotations, List<String> properties) {
Copy link
Contributor Author

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)) {
Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

Copy link
Member

@rozza rozza left a 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/PojoBuilderHelper.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");
Copy link
Member

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.

bson/src/main/org/bson/codecs/pojo/PojoBuilderHelper.java Outdated Show resolved Hide resolved
@@ -55,6 +55,7 @@
private String discriminator;
private String discriminatorKey;
private String idPropertyName;
private boolean isRecord;
Copy link
Contributor Author

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()) {
Copy link
Contributor Author

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> {
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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.

Copy link
Contributor

@jyemin jyemin left a 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 {
Copy link
Contributor

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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

bson/src/test/unit/org/bson/codecs/pojo/Student.java Outdated Show resolved Hide resolved
Copy link
Contributor

@jyemin jyemin left a 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.

@rozza
Copy link
Member

rozza commented Oct 8, 2020

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.

@jyemin
Copy link
Contributor

jyemin commented Oct 8, 2020

Also LGTM! Great work!

@bdeleonardis1
Copy link
Contributor Author

This branch has been pushed to the main repo and will be merged once Java 16 is released.

@cardouken
Copy link

cardouken commented May 27, 2021

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?

@jyemin
Copy link
Contributor

jyemin commented May 27, 2021

@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.

@cardouken
Copy link

@jyemin thanks for the quick update, looking forward to it coming out.

@nikammerlaan
Copy link

Any news on this now?

@jyemin
Copy link
Contributor

jyemin commented Nov 24, 2021

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.

@fmendezp
Copy link

@jyemin thanks for the several updates. Do you have any further information about the possibility of this moving forward?

@jyemin
Copy link
Contributor

jyemin commented Feb 28, 2022

We are still planning to move forward, but encountered a few more build complications. Apologies for the further delays.

@fmendezp
Copy link

Thank you for the update!
Happy to read that it is still planned to move forward. I understand that this topic comes with its share of complications. I hope it will be possible to solve them.

@hfoxy
Copy link

hfoxy commented Aug 8, 2022

Any further news on this?

@jyemin
Copy link
Contributor

jyemin commented Aug 8, 2022

See JAVA-3567. It was released in 4.6.0.

@hfoxy
Copy link

hfoxy commented Aug 8, 2022

I see, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
7 participants