Skip to content

Commit

Permalink
feat: mongodb-panache-reactive extension
Browse files Browse the repository at this point in the history
fix: improve test

- Use @MongoEntity
- Better SSE test

feat: optional support


chore: move to axle package


Merge reactive and blocking API into the same module


fix bug quarkusio#5885 for Axle


Remove the deleted reactive extension stage from the CI


Documentation guide


Apply suggestions from code review

Co-Authored-By: Guillaume Smet <[email protected]>
  • Loading branch information
loicmathieu and gsmet committed Feb 4, 2020
1 parent bb60329 commit eabf06c
Show file tree
Hide file tree
Showing 32 changed files with 3,972 additions and 26 deletions.
5 changes: 5 additions & 0 deletions bom/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@
<artifactId>quarkus-panache-common-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mongodb-panache-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mongodb-client-deployment</artifactId>
Expand Down
103 changes: 103 additions & 0 deletions docs/src/main/asciidoc/mongodb-panache.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,109 @@ data class Person @BsonCreator constructor (
): PanacheMongoEntity()
----

== Reactive Entities and Repositories

MongoDB with Panache allows using reactive style implementation for both entities and repositories.
For this, you need to use the Reactive variants when defining your entities : `ReactivePanacheMongoEntity` or `ReactivePanacheMongoEntityBase`,
and when defining your repositories: `ReactivePanacheMongoRepository` or `ReactivePanacheMongoRepositoryBase`.

The reactive variant of the `Person` class will be:

[source,java]
----
public class ReactivePerson extends ReactivePanacheMongoEntity {
public String name;
public LocalDate birth;
public Status status;
// return name as uppercase in the model
public String getName(){
return name.toUpperCase();
}
// store all names in lowercase in the DB
public void setName(String name){
this.name = name.toLowerCase();
}
}
----

You will have access to the same functionalities of the _imperative_ variant inside the reactive one: bson annotations, custom ID, PanacheQL, ...
But the methods on your entities or repositories will all return reactive types.

See the equivalent methods from the imperative example with the reactive variant:

[source,java]
----
// creating a person
ReactivePerson person = new ReactivePerson();
person.name = "Loïc";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;
// persist it
CompletionStage<Void> cs1 = person.persist();
person.status = Status.Dead;
// Your must call update() in order to send your entity modifications to MongoDB
CompletionStage<Void> cs2 = person.update();
// delete it
CompletionStage<Void> cs3 = person.delete();
// getting a list of all persons
CompletionStage<List<ReactivePerson>> allPersons = ReactivePerson.listAll();
// finding a specific person by ID
CompletionStage<ReactivePerson> personById = ReactivePerson.findById(personId);
// finding a specific person by ID via an Optional
CompletionStage<Optional<ReactivePerson>> optional = ReactivePerson.findByIdOptional(personId);
personById = optional.thenApply(o -> o.orElseThrow(() -> new NotFoundException()));
// finding all living persons
CompletionStage<List<ReactivePerson>> livingPersons = ReactivePerson.list("status", Status.Alive);
// counting all persons
CompletionStage<Long> countAll = ReactivePerson.count();
// counting all living persons
CompletionStage<Long> countAlive = ReactivePerson.count("status", Status.Alive);
// delete all living persons
CompletionStage<Long> deleteCount = ReactivePerson.delete("status", Status.Alive);
// delete all persons
deleteCount = ReactivePerson.deleteAll();
----

TIP: If you use MongoDB with Panache in conjunction with RESTEasy, you can directly return a reactive type inside your JAX-RS resource endpoint as RESTEasy will handle it correctly.

The same query facility exists for the reactive types, but the `stream()` methods act differently: they return a reactive stream `Publisher` instead of a `Stream`.

It allows more advanced reactive use cases, for example you can use it to send server-sent events (SSE) via RESTEasy:

[source,java]
----
import org.jboss.resteasy.annotations.SseElementType;
import org.reactivestreams.Publisher;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@GET
@Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS)
@SseElementType(MediaType.APPLICATION_JSON)
public Publisher<ReactivePerson> streamPersons() {
return ReactivePerson.streamAll();
}
----

TIP: `@SseElementType(MediaType.APPLICATION_JSON)` tells RESTEasy to serialize the object in JSON.


== How and why we simplify MongoDB API

When it comes to writing MongoDB entities, there are a number of annoying things that users have grown used to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,6 @@ protected void generateAccessorSetField(MethodVisitor mv, EntityField field) {
protected void generateAccessorGetField(MethodVisitor mv, EntityField field) {
mv.visitFieldInsn(Opcodes.GETFIELD, thisClass.getInternalName(), field.name, field.descriptor);
}

@Override
public void visitEnd() {
super.visitEnd();
}
}

public void collectFields(ClassInfo classInfo) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,15 @@
import io.quarkus.mongodb.panache.PanacheMongoRepository;
import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase;
import io.quarkus.mongodb.panache.ProjectionFor;
import io.quarkus.mongodb.panache.axle.ReactivePanacheMongoEntity;
import io.quarkus.mongodb.panache.axle.ReactivePanacheMongoEntityBase;
import io.quarkus.mongodb.panache.axle.ReactivePanacheMongoRepository;
import io.quarkus.mongodb.panache.axle.ReactivePanacheMongoRepositoryBase;
import io.quarkus.panache.common.deployment.PanacheFieldAccessEnhancer;
import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer;

public class PanacheResourceProcessor {
// blocking types
static final DotName DOTNAME_PANACHE_REPOSITORY_BASE = DotName.createSimple(PanacheMongoRepositoryBase.class.getName());
private static final DotName DOTNAME_PANACHE_REPOSITORY = DotName.createSimple(PanacheMongoRepository.class.getName());
static final DotName DOTNAME_PANACHE_ENTITY_BASE = DotName.createSimple(PanacheMongoEntityBase.class.getName());
Expand All @@ -55,6 +60,15 @@ public class PanacheResourceProcessor {
private static final DotName DOTNAME_PROJECTION_FOR = DotName.createSimple(ProjectionFor.class.getName());
private static final DotName DOTNAME_BSON_PROPERTY = DotName.createSimple(BsonProperty.class.getName());

// reactive types: Axle
static final DotName DOTNAME_AXLE_PANACHE_REPOSITORY_BASE = DotName
.createSimple(ReactivePanacheMongoRepositoryBase.class.getName());
private static final DotName DOTNAME_AXLE_PANACHE_REPOSITORY = DotName
.createSimple(ReactivePanacheMongoRepository.class.getName());
static final DotName DOTNAME_AXLE_PANACHE_ENTITY_BASE = DotName
.createSimple(ReactivePanacheMongoEntityBase.class.getName());
private static final DotName DOTNAME_AXLE_PANACHE_ENTITY = DotName.createSimple(ReactivePanacheMongoEntity.class.getName());

private static final DotName DOTNAME_OBJECT_ID = DotName.createSimple(ObjectId.class.getName());

private static final DotName DOTNAME_OBJECT = DotName.createSimple(Object.class.getName());
Expand Down Expand Up @@ -265,4 +279,58 @@ private void extractMappings(Map<String, String> classPropertyMapping, ClassInfo
extractMappings(classPropertyMapping, superClass, index);
}
}

@BuildStep
void buildAxle(CombinedIndexBuildItem index,
ApplicationIndexBuildItem applicationIndex,
BuildProducer<BytecodeTransformerBuildItem> transformers) throws Exception {

ReactivePanacheMongoRepositoryEnhancer daoEnhancer = new ReactivePanacheMongoRepositoryEnhancer(index.getIndex());
Set<String> daoClasses = new HashSet<>();
for (ClassInfo classInfo : index.getIndex().getAllKnownImplementors(DOTNAME_AXLE_PANACHE_REPOSITORY_BASE)) {
// Skip PanacheRepository
if (classInfo.name().equals(DOTNAME_AXLE_PANACHE_REPOSITORY))
continue;
if (PanacheRepositoryEnhancer.skipRepository(classInfo))
continue;
daoClasses.add(classInfo.name().toString());
}
for (ClassInfo classInfo : index.getIndex().getAllKnownImplementors(DOTNAME_AXLE_PANACHE_REPOSITORY)) {
if (PanacheRepositoryEnhancer.skipRepository(classInfo))
continue;
daoClasses.add(classInfo.name().toString());
}
for (String daoClass : daoClasses) {
transformers.produce(new BytecodeTransformerBuildItem(daoClass, daoEnhancer));
}

ReactivePanacheMongoEntityEnhancer modelEnhancer = new ReactivePanacheMongoEntityEnhancer(index.getIndex());
Set<String> modelClasses = new HashSet<>();
// Note that we do this in two passes because for some reason Jandex does not give us subtypes
// of PanacheMongoEntity if we ask for subtypes of PanacheMongoEntityBase
for (ClassInfo classInfo : index.getIndex().getAllKnownSubclasses(DOTNAME_AXLE_PANACHE_ENTITY_BASE)) {
if (classInfo.name().equals(DOTNAME_AXLE_PANACHE_ENTITY))
continue;
if (modelClasses.add(classInfo.name().toString()))
modelEnhancer.collectFields(classInfo);
}
for (ClassInfo classInfo : index.getIndex().getAllKnownSubclasses(DOTNAME_AXLE_PANACHE_ENTITY)) {
if (modelClasses.add(classInfo.name().toString()))
modelEnhancer.collectFields(classInfo);
}
for (String modelClass : modelClasses) {
transformers.produce(new BytecodeTransformerBuildItem(modelClass, modelEnhancer));
}

if (!modelEnhancer.entities.isEmpty()) {
PanacheFieldAccessEnhancer panacheFieldAccessEnhancer = new PanacheFieldAccessEnhancer(
modelEnhancer.getModelInfo());
for (ClassInfo classInfo : applicationIndex.getIndex().getKnownClasses()) {
String className = classInfo.name().toString();
if (!modelClasses.contains(className)) {
transformers.produce(new BytecodeTransformerBuildItem(className, panacheFieldAccessEnhancer));
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package io.quarkus.mongodb.panache.deployment;

import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import org.bson.codecs.pojo.annotations.BsonIgnore;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.IndexView;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import io.quarkus.gizmo.DescriptorUtils;
import io.quarkus.mongodb.panache.axle.runtime.ReactiveMongoOperations;
import io.quarkus.panache.common.deployment.EntityField;
import io.quarkus.panache.common.deployment.EntityModel;
import io.quarkus.panache.common.deployment.MetamodelInfo;
import io.quarkus.panache.common.deployment.PanacheEntityEnhancer;

public class ReactivePanacheMongoEntityEnhancer extends PanacheEntityEnhancer<MetamodelInfo<EntityModel<EntityField>>> {
public final static String MONGO_OPERATIONS_NAME = ReactiveMongoOperations.class.getName();
public final static String MONGO_OPERATIONS_BINARY_NAME = MONGO_OPERATIONS_NAME.replace('.', '/');

private static final DotName DOTNAME_BSON_IGNORE = DotName.createSimple(BsonIgnore.class.getName());

final Map<String, EntityModel> entities = new HashMap<>();

public ReactivePanacheMongoEntityEnhancer(IndexView index) {
super(index, PanacheResourceProcessor.DOTNAME_AXLE_PANACHE_ENTITY_BASE);
modelInfo = new MetamodelInfo<>();
}

@Override
public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) {
return new PanacheMongoEntityClassVisitor(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo);
}

static class PanacheMongoEntityClassVisitor extends PanacheEntityClassVisitor<EntityField> {

public PanacheMongoEntityClassVisitor(String className, ClassVisitor outputClassVisitor,
MetamodelInfo<EntityModel<EntityField>> modelInfo, ClassInfo panacheEntityBaseClassInfo) {
super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo);
}

@Override
protected void injectModel(MethodVisitor mv) {
mv.visitLdcInsn(thisClass);
}

@Override
protected String getModelDescriptor() {
return "Ljava/lang/Class;";
}

@Override
protected String getPanacheOperationsBinaryName() {
return MONGO_OPERATIONS_BINARY_NAME;
}

@Override
protected void generateAccessorSetField(MethodVisitor mv, EntityField field) {
mv.visitFieldInsn(Opcodes.PUTFIELD, thisClass.getInternalName(), field.name, field.descriptor);
}

@Override
protected void generateAccessorGetField(MethodVisitor mv, EntityField field) {
mv.visitFieldInsn(Opcodes.GETFIELD, thisClass.getInternalName(), field.name, field.descriptor);
}
}

public void collectFields(ClassInfo classInfo) {
EntityModel<EntityField> entityModel = new EntityModel<>(classInfo);
for (FieldInfo fieldInfo : classInfo.fields()) {
String name = fieldInfo.name();
if (Modifier.isPublic(fieldInfo.flags()) && !fieldInfo.hasAnnotation(DOTNAME_BSON_IGNORE)) {
entityModel.addField(new EntityField(name, DescriptorUtils.typeToString(fieldInfo.type())));
}
}
modelInfo.addEntityModel(entityModel);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.quarkus.mongodb.panache.deployment;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;

import io.quarkus.mongodb.panache.axle.ReactivePanacheMongoRepository;
import io.quarkus.mongodb.panache.axle.ReactivePanacheMongoRepositoryBase;
import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer;

public class ReactivePanacheMongoRepositoryEnhancer extends PanacheRepositoryEnhancer {
public final static DotName PANACHE_REPOSITORY_BASE_NAME = DotName
.createSimple(ReactivePanacheMongoRepositoryBase.class.getName());

public final static DotName PANACHE_REPOSITORY_NAME = DotName.createSimple(ReactivePanacheMongoRepository.class.getName());

public ReactivePanacheMongoRepositoryEnhancer(IndexView index) {
super(index, PanacheResourceProcessor.DOTNAME_AXLE_PANACHE_REPOSITORY_BASE);
}

@Override
public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) {
return new PanacheMongoRepositoryClassVisitor(className, outputClassVisitor, panacheRepositoryBaseClassInfo,
this.indexView);
}

static class PanacheMongoRepositoryClassVisitor extends PanacheRepositoryClassVisitor {

public PanacheMongoRepositoryClassVisitor(String className, ClassVisitor outputClassVisitor,
ClassInfo panacheRepositoryBaseClassInfo, IndexView indexView) {
super(className, outputClassVisitor, panacheRepositoryBaseClassInfo, indexView);
}

@Override
protected DotName getPanacheRepositoryDotName() {
return PANACHE_REPOSITORY_NAME;
}

@Override
protected DotName getPanacheRepositoryBaseDotName() {
return PANACHE_REPOSITORY_BASE_NAME;
}

@Override
protected String getPanacheOperationsBinaryName() {
return ReactivePanacheMongoEntityEnhancer.MONGO_OPERATIONS_BINARY_NAME;
}

@Override
protected void injectModel(MethodVisitor mv) {
// inject Class
mv.visitLdcInsn(entityType);
}

@Override
protected String getModelDescriptor() {
return "Ljava/lang/Class;";
}
}
}
Loading

0 comments on commit eabf06c

Please sign in to comment.