
by
Johannes Schüth
Lead Developer of Gentics Mesh
Headless CMS
Gentics builds CMS since 2000
Started in 11/2014
First public release 04/2016
Open source since 07/2016
back-end only CMS
Access via REST API or GraphQL
multi language support
binary storage
image resizing
manage contents in tree structures
link handling
versioning
searchable contents
Lightweight and modern web framework
Embeddable Graph Database
Contents must be searchable
Scalability
Eclipse Vert.x
OrientDB Graph Database
Dagger2
RxJava2
GraphQL
Elasticsearch
Hazelcast
Swiss army knife for HTTP
Reactive Framework
Async API (Datastorage, Filesystem, HTTP)
Modular / General purpose
HTTP routing
Session, Cookie, Upload handling
Websockets
Authentication handlers
Vertx vertx = Vertx.vertx();
Router router = Router.router(vertx);
router.route("/hello").handler(rc -> {
rc.response().end("World");
});
vertx.createHttpServer()
.requestHandler(router::accept)
.listen(8080);
Vert.x is very flexible
Multiple ways to setup a REST API project
Routes are setup once and shared for all verticles
Routes are setup once and shared for all verticles
Reactive Extensions for the JVM
MeshRestClient client = MeshRestClient.create("localhost", Vertx.vertx());
List<String> names = Arrays.asList("Iron Man",
"Captain America", "Star Lord",
"Black Widow", "Hulk");
client.findProjectByName("MCU").toSingle().flatMapCompletable(project -> {
return Observable.fromIterable(names).flatMapSingle(name -> {
NodeCreateRequest request = new NodeCreateRequest();
request.setParentNodeUuid(project.getRootNode().getUuid());
request.setLanguage("en");
return client.createNode("MCU", request).toSingle();
}).ignoreElements();
}).subscribe();
Query language used to query nested data structures
First released 2015
Developed by Facebook
Alternative to REST
{
node(path: "/aircrafts/space-shuttle") {
uuid
fields {
... on vehicle {
weight
price
slug
}
}
}
}
{
"data": {
"node": {
"uuid": "f915b16fa68f40e395b16fa68f10e32d",
"fields": {
"weight": 22700,
"price": 192000000000,
"slug": "space-shuttle"
}
}
}
}
Solves problems
Overfetching
Underfetching
Loading deeply nested data structures
The response of the server contains more information then you need
→ Superfluous data still needs to be loaded to complete the request
→ Causes additional delays and load on the system
The response of the server is lacking information you need
→ Execute additional requests to load the data
→ Causes additional delays
Caching responses is not that easy
Error handling done via JSON
Not easy to create Types/POJOs for results
Java API to create GraphQL schema
Github: github.com/graphql-java/graphql-java
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>8.0</version>
</dependency>
The Conference
references a Workshop
.
public class Conference {
private final Workshop workshop;
public Conference(Workshop workshop) {
this.workshop = workshop;
}
public Workshop getWorkshop() {
return workshop;
}
}
A Workshop
has an id
and a name
.
public class Workshop {
private final String name;
private final long id;
public Workshop(long id, String name) {
this.id = id;
this.name = name;
}
public String getName() {
return name;
}
public long getId() {
return id;
}
}
public static GraphQLSchema createSchema() {
return GraphQLSchema.newSchema()
.query(createQueryType())
.build();
}
private static GraphQLObjectType createQueryType() {
return newObject().name("QueryType")
// .workshop
.field(newFieldDefinition().name("workshop")
.type(createWorkshopType())
.dataFetcher((env) -> {
Conference root = env.getSource();
return root.getWorkshop();
}))
.build();
}
private static GraphQLObjectType createWorkshopType() {
return newObject().name("Workshop")
.description("A workshop element")
// .id
.field(newFieldDefinition().name("id")
.description("The id of the workshop.")
.type(new GraphQLNonNull(GraphQLLong)))
// .name
.field(newFieldDefinition().name("name")
.description("The name of the workshop.")
.type(GraphQLString))
.build();
}
{
workshop {
name, id
}
}
GraphQLSchema schema = ConferenceSchema.createSchema();
Workshop demo = new Workshop(42, "Gentics Mesh Tech Stack");
Conference rootElement = new Conference(demo);
// Execute the query
GraphQL graphQL = newGraphQL(schema).build();
String operation = null;
Map<String, Object> variables = null;
ExecutionInput in =
new ExecutionInput(query, operation, queryJson, rootElement, variables);
ExecutionResult result = graphQL.execute(in);
Map<String, Object> data = (Map<String, Object>) result.getData();
System.out.println(new JsonObject(data).encodePrettily());
Root element of the graph is starting point of query
Data fetchers can load directly referenced data
…
.dataFetcher((env) -> {
RootElement root = env.getSource();
return root.getDemo();
}))
…
Data fetchers directly traverses the GraphDB
Highly efficient
Convenient for profiling
CPU profiling very expressive
Easy to spot potential bottlenecks
JProfiler example
Open Source
Vendor Agnostic API
Supported by many graph database vendors
Gremlin Traversal Language
TinkerGraph graph = TinkerGraph.open();
GraphTraversalSource g = graph.traversal();
Vertex johannes = g.addV("name", "johannes").next();
Vertex tinkerpop = g.addV("name", "tinkerpop").next();
Edge egde = johannes.addEdge("presents", tinkerpop);
Object Graph Mapper library (OGM)
Provides Java API to model your Graph Domain using classes
Java based Graph Database
First release in 2010
Directly supports Tinkerpop API
Supports Master/Master replication
Dagger is a fully static, compile-time dependency injection framework for both Java.
Now maintained by Google (previously Square)
Loosely coupled implementations
Provide dependencies in your code
Manage dependency hierarchies
The component describes the root of the dependency tree
@Singleton
@Component(modules = { AppModule.class })
public interface AppComponent {
JsonObject configuration();
HelloService hello();
@Component.Builder
interface Builder {
@BindsInstance
Builder configuration(JsonObject configuration);
AppComponent build();
}
}
@Singleton
public class HelloService {
@Inject
JsonObject configuration;
@Inject
public HelloService() {
}
public String getResult() {
return configuration.getString("hello");
}
}
AppComponent → DaggerAppComponent (generated)
JsonObject config = new JsonObject();
config.put("hello", "world");
AppComponent app = DaggerAppComponent.builder()
.configuration(config)
.build();
System.out.println(app.hello().getResult());
<dependencies>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<version>${dagger.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<verbose>true</verbose>
<source>8</source>
<target>8</target>
<forceJavacCompilerUse>true</forceJavacCompilerUse>
</configuration>
<dependencies>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<version>${dagger.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
IDE integration can be tricky
Code regeneration
Source of dagger DI issues sometimes hard to trace
DI tree isolated
AppComponent app = DaggerAppComponent.builder()
.configuration(config)
.build();
Java based
Highly scaleable
Based on Apache Lucene
Store Gentics Mesh elements in indices (Users, Nodes, Groups)
Dynamic mapping can cause issues when input data is dynamic
Index types are gradually being deprecated
Differential sync per index
Assign hashsum to each document
Collect hashsums of all elements in the graph
Collect hashsums of all elements in the index
Compare using Google Guava:
com.google.common.collect.Maps#difference
Map<String, String> sourceVersions = loadVersionsFromGraph();
Map<String, String> sinkVersions = loadVersionsFromIndex(indexName);
MapDifference<String, String> diff = Maps.difference(sourceVersions,
sinkVersions);
Set<String> needInsertionInES = diff.entriesOnlyOnLeft().keySet();
Set<String> needRemovalInES = diff.entriesOnlyOnRight().keySet();
Set<String> needUpdate = diff.entriesDiffering().keySet();