Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions core/src/main/java/dev/morphia/mapping/CritterMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ public CritterMapper(CritterMapper other) {
* this shares immutable {@code CritterEntityModel} references and creates a new
* {@code DiscriminatorLookup} rather than delegating to {@code AbstractMapper}'s copy
* constructor.
* <p>
* Already-mapped entities are cloned as they are. Entities mapped later through the copy reuse the original's
* runtime-generated classes only if {@code config} generates the same bytecode (same naming, discriminator, property
* discovery and annotation providers); otherwise the copy generates its own.
* </p>
*
* @param other the original to clone the entity graph from
* @param config the config the new mapper should report/operate under
Expand All @@ -129,7 +134,7 @@ public CritterMapper(CritterMapper other) {
@MorphiaInternal
public CritterMapper(CritterMapper other, MorphiaConfig config) {
super(config, other.classLoader);
this.runtimeModels = other.runtimeModels;
this.runtimeModels = RuntimeModels.forCopy(other.runtimeModels, other.config, config, other.classLoader);
this.generator = new CritterGenerator(this);
this.fallbackTypes = other.fallbackTypes;
this.listeners.addAll(other.listeners);
Expand Down Expand Up @@ -233,7 +238,8 @@ private EntityModel fallbackToReflection(Class<?> type) {
* {@link CritterClassLoader} is created the first time a model has to be generated at runtime; when every model is
* pre-generated, it is never needed.
* <p>
* Copies of a mapper share the same instance. So do separate mappers with the same parent loader and an equivalent
* Copies of a mapper share the same instance unless the copy's configuration generates different bytecode. So do separate mappers with
* the same parent loader and an equivalent
* configuration (see {@link #generationKey(MorphiaConfig)}), so each entity is generated once rather than once per
* mapper. Instances are cached weakly: once no mapper refers to one, its loader and classes can be collected.
*/
Expand Down Expand Up @@ -272,6 +278,34 @@ private static RuntimeModels forConfig(MorphiaConfig config, ClassLoader parent)
}
}

/**
* Picks the runtime models for a copy of a mapper. The copy keeps the original's instance when its configuration
* generates the same bytecode; otherwise entities first mapped through the copy would get classes generated under
* the original's settings.
*
* @param original the original mapper's runtime models
* @param originalConfig the original mapper's configuration
* @param config the copy's configuration
* @param parent the parent class loader
* @return the runtime models for the copy
*/
private static RuntimeModels forCopy(RuntimeModels original, MorphiaConfig originalConfig, MorphiaConfig config,
ClassLoader parent) {
if (config == originalConfig) {
return original;
}
List<Object> originalKey = generationKey(originalConfig);
if (originalKey != null && originalKey.equals(generationKey(config))) {
return original;
}
if (parent instanceof CritterClassLoader) {
// The original may already have defined its models in this loader under the same names, and the loader
// would hand those back; generate the copy's models in a child loader of their own.
return new RuntimeModels(new CritterClassLoader(parent));
}
return forConfig(config, parent);
}

/**
* Returns the model class for {@code type}, generating it the first time any mapper sharing this instance asks.
*/
Expand Down
54 changes: 54 additions & 0 deletions core/src/test/java/dev/morphia/mapping/TestCritterMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,60 @@ public void testDifferentNamingDoesNotShareRuntimeModels() {
Assertions.assertEquals("Name", title.getProperty("name").getMappedName());
}

@Test
public void testCopyWithDifferentNamingGeneratesItsOwnModels() {
CritterMapper original = mapper();
CritterMapper copy = new CritterMapper(original, MorphiaConfig.load()
.mapper(MapperType.CRITTER)
.propertyNaming(NamingStrategy.title()));

EntityModel copied = copy.mapEntity(CritterMapperTestEntity.class);
EntityModel camelCase = original.mapEntity(CritterMapperTestEntity.class);

Assertions.assertInstanceOf(CritterClassLoader.class, copied.getClass().getClassLoader(),
"Expected a runtime-generated model");
Assertions.assertEquals("Name", copied.getProperty("name").getMappedName(),
"An entity first mapped through the copy must use the copy's naming");
Assertions.assertEquals("name", camelCase.getProperty("name").getMappedName());
Assertions.assertNotSame(camelCase.getClass(), copied.getClass());
}

@Test
public void testCopyWithDifferentNamingUnderCritterClassLoaderParent() {
CritterClassLoader parent = new CritterClassLoader(getClass().getClassLoader());
CritterMapper original = new CritterMapper(MorphiaConfig.load().mapper(MapperType.CRITTER), parent);
CritterMapper copy = new CritterMapper(original, MorphiaConfig.load()
.mapper(MapperType.CRITTER)
.propertyNaming(NamingStrategy.title()));

EntityModel camelCase = original.mapEntity(CritterMapperTestEntity.class);
EntityModel copied = copy.mapEntity(CritterMapperTestEntity.class);

Assertions.assertInstanceOf(CritterClassLoader.class, copied.getClass().getClassLoader(),
"Expected a runtime-generated model");
Assertions.assertEquals("name", camelCase.getProperty("name").getMappedName());
Assertions.assertEquals("Name", copied.getProperty("name").getMappedName(),
"The copy must not reuse the model the original defined in the shared parent loader");
Assertions.assertNotSame(camelCase.getClass(), copied.getClass());
}

@Test
public void testCopyWithDifferentDatabaseSharesRuntimeModels() {
CritterMapper original = mapper();
CritterMapper copy = new CritterMapper(original, MorphiaConfig.load()
.mapper(MapperType.CRITTER)
.database("copy_database"));

EntityModel copied = copy.mapEntity(CritterMapperTestEntity.class);
EntityModel model = original.mapEntity(CritterMapperTestEntity.class);

Assertions.assertInstanceOf(CritterClassLoader.class, copied.getClass().getClassLoader(),
"Expected a runtime-generated model");
Assertions.assertSame(model.getClass(), copied.getClass(),
"A copy that only changes operational settings should reuse the generated model class");
Assertions.assertNotSame(model, copied);
}

@Test
public void testCustomNamingDoesNotShareRuntimeModels() {
NamingStrategy upperCase = new NamingStrategy() {
Expand Down
Loading