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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.morphia.test.models.records;

import dev.morphia.annotations.Entity;

/**
* An embedded record in a package listed in {@code morphia.packages}, so critter-maven generates its model ahead of time.
*/
@Entity
public record AotRecordAddress(String street, String city) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package dev.morphia.test.models.records;

import java.util.List;
import java.util.Objects;

import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;

import org.bson.types.ObjectId;

/**
* A regular class entity that embeds a record, in a package listed in {@code morphia.packages}.
*/
@Entity("aot_record_holders")
public class AotRecordHolder {
@Id
private ObjectId id;
private AotRecordAddress address;
private List<AotRecordAddress> history;

public AotRecordHolder() {
}

public AotRecordHolder(AotRecordAddress address, List<AotRecordAddress> history) {
this.address = address;
this.history = history;
}

public ObjectId getId() {
return id;
}

public AotRecordAddress getAddress() {
return address;
}

public List<AotRecordAddress> getHistory() {
return history;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AotRecordHolder that)) {
return false;
}
return Objects.equals(id, that.id) && Objects.equals(address, that.address) && Objects.equals(history, that.history);
}

@Override
public int hashCode() {
return Objects.hash(id, address, history);
}

@Override
public String toString() {
return "AotRecordHolder{id=" + id + ", address=" + address + ", history=" + history + "}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.morphia.test.models.records;

import java.util.List;

import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Property;

import org.bson.types.ObjectId;

/**
* A record entity in a package listed in {@code morphia.packages}, so critter-maven generates its model ahead of time.
*/
@Entity("aot_record_people")
public record AotRecordPerson(@Id ObjectId id, String name, @Property("years") int age, List<String> nicknames) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.morphia.test.records;

import dev.morphia.annotations.Entity;

/**
* An embedded record outside the packages listed in {@code morphia.packages}, so the critter mapper generates its model at runtime.
*/
@Entity
public record RuntimeRecordAddress(String street, String city) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package dev.morphia.test.records;

import java.util.List;
import java.util.Objects;

import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;

import org.bson.types.ObjectId;

/**
* A regular class entity that embeds a record, outside the packages listed in {@code morphia.packages}.
*/
@Entity("runtime_record_holders")
public class RuntimeRecordHolder {
@Id
private ObjectId id;
private RuntimeRecordAddress address;
private List<RuntimeRecordAddress> history;

public RuntimeRecordHolder() {
}

public RuntimeRecordHolder(RuntimeRecordAddress address, List<RuntimeRecordAddress> history) {
this.address = address;
this.history = history;
}

public ObjectId getId() {
return id;
}

public RuntimeRecordAddress getAddress() {
return address;
}

public List<RuntimeRecordAddress> getHistory() {
return history;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof RuntimeRecordHolder that)) {
return false;
}
return Objects.equals(id, that.id) && Objects.equals(address, that.address) && Objects.equals(history, that.history);
}

@Override
public int hashCode() {
return Objects.hash(id, address, history);
}

@Override
public String toString() {
return "RuntimeRecordHolder{id=" + id + ", address=" + address + ", history=" + history + "}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.morphia.test.records;

import java.util.List;

import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Property;

import org.bson.types.ObjectId;

/**
* A record entity outside the packages listed in {@code morphia.packages}, so the critter mapper generates its model at runtime.
*/
@Entity("runtime_record_people")
public record RuntimeRecordPerson(@Id ObjectId id, String name, @Property("years") int age, List<String> nicknames) {
}
114 changes: 114 additions & 0 deletions core/src/test/java/dev/morphia/test/records/TestRecordEntities.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package dev.morphia.test.records;

import java.util.List;

import dev.morphia.critter.Critter;
import dev.morphia.critter.CritterClassLoader;
import dev.morphia.mapping.MapperType;
import dev.morphia.mapping.codec.pojo.EntityModel;
import dev.morphia.mapping.codec.pojo.critter.CritterEntityModel;
import dev.morphia.test.TestBase;
import dev.morphia.test.models.records.AotRecordAddress;
import dev.morphia.test.models.records.AotRecordHolder;
import dev.morphia.test.models.records.AotRecordPerson;

import org.bson.Document;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import static dev.morphia.query.filters.Filters.eq;

/**
* Round-trips Java records through the datastore. The {@code Aot*} fixtures live in a package listed in
* {@code morphia.packages}, so critter-maven pre-generates their models; the {@code Runtime*} fixtures live here, outside those
* packages, so the critter mapper generates their models at runtime. Under the reflection mapper both sets use reflection.
*/
public class TestRecordEntities extends TestBase {
private enum Tier {
AOT,
RUNTIME
}

@Test
public void aotRecordEntity() {
AotRecordPerson person = new AotRecordPerson(new ObjectId(), "Ada", 36, List.of("Countess", "Enchantress"));
getDs().save(person);

assertTier(AotRecordPerson.class, Tier.AOT);
assertStoredShape(getDs().getCollection(AotRecordPerson.class).getNamespace().getCollectionName(), person.id());
Assertions.assertEquals(person, getDs().find(AotRecordPerson.class).filter(eq("_id", person.id())).first());
Assertions.assertEquals(person, getDs().find(AotRecordPerson.class).filter(eq("age", 36)).first());
}

@Test
public void runtimeRecordEntity() {
RuntimeRecordPerson person = new RuntimeRecordPerson(new ObjectId(), "Grace", 85, List.of("Amazing Grace"));
getDs().save(person);

assertTier(RuntimeRecordPerson.class, Tier.RUNTIME);
assertStoredShape(getDs().getCollection(RuntimeRecordPerson.class).getNamespace().getCollectionName(), person.id());
Assertions.assertEquals(person, getDs().find(RuntimeRecordPerson.class).filter(eq("_id", person.id())).first());
Assertions.assertEquals(person, getDs().find(RuntimeRecordPerson.class).filter(eq("age", 85)).first());
}

@Test
public void aotEmbeddedRecord() {
AotRecordHolder holder = new AotRecordHolder(new AotRecordAddress("1 Main St", "Springfield"),
List.of(new AotRecordAddress("2 Elm St", "Shelbyville"), new AotRecordAddress("3 Oak St", "Capital City")));
getDs().save(holder);

assertTier(AotRecordHolder.class, Tier.AOT);
assertTier(AotRecordAddress.class, Tier.AOT);
Assertions.assertEquals(holder, getDs().find(AotRecordHolder.class).filter(eq("_id", holder.getId())).first());
}

@Test
public void runtimeEmbeddedRecord() {
RuntimeRecordHolder holder = new RuntimeRecordHolder(new RuntimeRecordAddress("1 Main St", "Springfield"),
List.of(new RuntimeRecordAddress("2 Elm St", "Shelbyville"), new RuntimeRecordAddress("3 Oak St", "Capital City")));
getDs().save(holder);

assertTier(RuntimeRecordHolder.class, Tier.RUNTIME);
assertTier(RuntimeRecordAddress.class, Tier.RUNTIME);
Assertions.assertEquals(holder, getDs().find(RuntimeRecordHolder.class).filter(eq("_id", holder.getId())).first());
}

/**
* Fails if the critter mapper silently fell back to a different tier, which would leave the tier this test is named for
* untested.
*/
private void assertTier(Class<?> type, Tier tier) {
EntityModel model = getMapper().getEntityModel(type);
Assertions.assertNotNull(model, "No model for " + type.getName());
if (morphiaConfig.mapper() == MapperType.REFLECTION) {
Assertions.assertFalse(model instanceof CritterEntityModel,
"Expected a reflection model for " + type.getName() + " but got " + model.getClass().getName());
return;
}
Assertions.assertInstanceOf(CritterEntityModel.class, model,
"Expected a critter model for " + type.getName() + " but got " + model.getClass().getName());
// Builds that don't run critter-maven's generate-test-models (e.g. pull-request CI) have no pre-generated
// models, so an AOT fixture is generated at runtime there. Expect AOT exactly when its model is on the classpath.
Tier expected = tier == Tier.AOT && !hasPregeneratedModel(type) ? Tier.RUNTIME : tier;
boolean runtimeGenerated = model.getClass().getClassLoader() instanceof CritterClassLoader;
Assertions.assertEquals(expected == Tier.RUNTIME, runtimeGenerated,
"Expected a " + expected + " critter model for " + type.getName() + " but got " + model.getClass().getName()
+ " from " + model.getClass().getClassLoader());
}

/**
* @return true if critter-maven pre-generated a model for {@code type}, using the name {@code CritterMapper} looks up
*/
private static boolean hasPregeneratedModel(Class<?> type) {
String modelClass = Critter.critterPackage(type) + "." + type.getSimpleName() + "EntityModel";
return type.getClassLoader().getResource(modelClass.replace('.', '/') + ".class") != null;
}

private void assertStoredShape(String collection, ObjectId id) {
Document stored = getDatabase().getCollection(collection).find(new Document("_id", id)).first();
Assertions.assertNotNull(stored, "Nothing stored in " + collection);
Assertions.assertTrue(stored.containsKey("years"), "Expected the renamed component to be stored as 'years': " + stored);
Assertions.assertFalse(stored.containsKey("age"), "Expected no 'age' key: " + stored);
}
}