Skip to content

Add support for factory methods to instantiate entities #2534

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

Closed
wants to merge 12 commits into from

Conversation

mp911de
Copy link
Member

@mp911de mp911de commented Jan 18, 2022

We now support static factory methods to instantiate entities. Factory methods can be disambiguated by annotating these with @Factory.

// using factory method
class FactoryPerson {

	private final String firstname, lastname;

	private FactoryPerson(String firstname, String lastname) {
		this.firstname = firstname;
		this.lastname = lastname;
	}

	public static FactoryPerson of(String firstname, String lastname) {
		return new FactoryPerson(firstname, lastname);
	}
}

// using annotated factory method
class FactoryMethodsPerson {

	private final String firstname, lastname;

	private FactoryMethodsPerson(String firstname, String lastname) {
		this.firstname = firstname;
		this.lastname = lastname;
	}

	public static FactoryMethodsPerson of(String firstname) {
		return new FactoryMethodsPerson(firstname, "unknown");
	}

	@Factory
	public static FactoryMethodsPerson of(String firstname, String lastname) {
		return new FactoryMethodsPerson(firstname, lastname);
	}
}

// using annotated constructor
class ConstructorPerson {

	private final String firstname, lastname;

	@PersistenceConstructor
	private ConstructorPerson(String firstname, String lastname) {
		this.firstname = firstname;
		this.lastname = lastname;
	}

	public static ConstructorPerson of(String firstname, String lastname) {
		return new ConstructorPerson(firstname, lastname);
	}
}

Closes #2476

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jan 18, 2022
Copy link
Member

@odrotbohm odrotbohm left a comment

Choose a reason for hiding this comment

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

A couple of inline comments. I am not entirely sure about the naming of @Factory as that collides with the concept of a factory type (as in: the pattern).

if (persistenceConstructor == null || Modifier.isPrivate(persistenceConstructor.getConstructor().getModifiers())) {
var entityCreator = entity.getEntityCreator();

if (entityCreator == null) {
return true;
}

Copy link
Member

Choose a reason for hiding this comment

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

Could this be a method on EntityCreator with the different ways of looking this up living in the implementations?

Copy link
Member Author

Choose a reason for hiding this comment

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

I considered the future arrangements where we might have no reflection access to constructors/methods (e.g. AOT/Graal Native that come with a different creator mechanism for entities). So I'd leave the reflective bits in the calling code.

Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't this be a different implementation of EntityInstantiator that could simply not call these methods? Or a decorator in between EntityCreator and the actual implementation instance?

Copy link
Member Author

Choose a reason for hiding this comment

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

Likely it would be a different EntityInstantiator that is implemented using generated code (AOT) per entity type. In our AOT Build-time POC a single class was implementing TypeInformation, EntityInstantiator and a few other interfaces to keep all concerns regarding a single class in a single place.


if (constructor != null && constructor.isEnclosingClassParameter(parameter)) {
return (T) parent;
if (creator instanceof PreferredConstructor) {
Copy link
Member

Choose a reason for hiding this comment

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

Could this be moved to a callback on EntityCreator with the default just returning false?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hum, enclosing class is only applicable in the context of constructors and non-static inner classes. We have a single call to that method so it doesn't seem to be a good fit for the general API.

Copy link
Member

Choose a reason for hiding this comment

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

I thought about broadening the name of the method to something like isParentParameter() and have that notion of "enclosing class parameter" move into the PersistenceConstructor implementation. Methods actually have something similar to that synthetic parameter, called "receiver parameters" that don't appear on the call side of the invocation: https://www.logicbig.com/tutorials/core-java-tutorial/java-8-enhancements/explicit_receiver_parameters.html. Not saying we should support those, but it's not as conceptually odd as it seems.

Copy link
Member Author

@mp911de mp911de Jan 18, 2022

Choose a reason for hiding this comment

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

thought about broadening the name of the method to something like isParentParameter()

That makes sense. Let me handle that as a separate commit.

params[i++] = provider.getParameterValue(parameter);
}

if (creator instanceof FactoryMethod method) {
Copy link
Member

Choose a reason for hiding this comment

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

Again, I think this should be on the EntityCreator interface to avoid the instance of checks and pulling out implementation details.

* @author Mark Paluch
* @since 3.0
*/
public final class FactoryMethod<T, P extends PersistentProperty<P>> extends EntityCreatorSupport<T, P> {
Copy link
Member

Choose a reason for hiding this comment

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

Wonder if we could get as far as avoiding having to expose these types (just as PreferredConstructor) publically, now that EntityCreator is available.

Copy link
Member Author

Choose a reason for hiding this comment

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

Depends on the design really. A FactoryMethod/PreferredConstructor makes only sense when there's reflective access. With our separation where we externalize EntityInstantiators we always leak implementation details. EntityCreator reduces the concept of entity creation to the actual description of input parameters without actually defining the approach (reflection, constructor/factory method, a provided lambda) how entities are being created. Exposing Executable would already assume that we use reflection information in some sense to create entities.

Copy link
Member

Choose a reason for hiding this comment

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

I get that, but most places in which the reflection API is used are not for the actual (reflective) invocation of those methods, but the inspection and decisions triggered by the outcome of that. Even in an AOT scenario, we would need that, wouldn't we? It feels pretty brittle to see all those instance of checks in different places, as they stop working as soon as you wrap the original instance in some decorator. The entire detection process leading to the creation of FactoryMethod/PreferredConstructor is reflection-based. If we avoid reflection happening in front of those instances, it's much easier to potentially replace the entire detection process and instances. For example, by an instance of EntityCreator that will just obtain that metadata from a different place without using any runtime reflection at all.

@@ -64,11 +67,21 @@

private volatile Map<TypeInformation<?>, EntityInstantiator> entityInstantiators = new HashMap<>(32);

private final boolean fallbackToReflectionOnError;
Copy link
Member

Choose a reason for hiding this comment

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

Introducing this flag seems to be related but not strictly required, is it?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is for testing purposes only. Tests were green during development although class-generated entity creation was broken and the creation fell back to reflective instance creation.

@mp911de
Copy link
Member Author

mp911de commented Jan 18, 2022

@Factory was the initial name. Now that we have a bit of code to back up the discussion, we can become creative about naming. @FactoryMethod would express as well what's meant.

@odrotbohm odrotbohm self-assigned this Jan 28, 2022
@odrotbohm odrotbohm removed the status: waiting-for-triage An issue we've not yet triaged label Jan 28, 2022
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jan 28, 2022
@odrotbohm
Copy link
Member

Thanks for all the work implementing this, Mark. As a final step, I suggest a bit of polishing in the annotation model. We should expose a canonical annotation to declare both constructors and factory methods as "creator". Long term, it doesn't really make sense to go with @PersistenceConstructor and @FactoryMethod as they're not really symmetric names and there's no real need for two different annotations.

Proposals:

  • Promote @EntityCreatorAnnotation to be the primary user-facing annotation, renaming it to @PersistenceCreator. The name makes obvious that the annotated element is used to create instances of that type by the persistence framework. I thought about @ObjectCreator, too, but that seems too generic.
  • Remove @FactoryMethod entirely, as it's obsolete if we want to move to a canonical annotation.
  • Deprecate @PersistenceConstructor in favor of @PersistenceCreator. Potential removal candidate versions are 3.0 (assuming we introduce this in 2.7) or 3.1.

Feel free to chime in if you'd like to veto those steps. Unless I get any until Monday, I'll proceed with the changes as proposed.

@mp911de
Copy link
Member Author

mp911de commented Jan 28, 2022

Sounds good to have @PersistenceCreator as top-level annotation and move towards Persistence… instead of Entity….

Move to @PersistenceCreator as canonical annotation to explicitly express constructors and methods to be used to create domain object instances from persistence operations. Removed @factorymethod as it's not needed anymore. @PersistenceConstructor is now deprecated.

Renamed EntityCreatorMetadata(Support|Discoverer) to InstanceCreatorMetadata(Support|Discoverer) to avoid further manifestation of the notion of an entity in the metamodel as it's not used to only handle entities.
@odrotbohm odrotbohm force-pushed the issue/entityfactory branch from 6b77868 to 14c150f Compare January 28, 2022 12:00
@odrotbohm odrotbohm linked an issue Feb 15, 2022 that may be closed by this pull request
@odrotbohm odrotbohm removed the status: waiting-for-triage An issue we've not yet triaged label Feb 16, 2022
@odrotbohm
Copy link
Member

That's applied to 2.7 and the 3.0 branch.

@odrotbohm odrotbohm closed this Feb 16, 2022
@mp911de mp911de deleted the issue/entityfactory branch March 2, 2022 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Investigate factory methods as alternative entity creation mechanism
3 participants