Upgrade to Pro — share decks privately, control downloads, hide ads and more …

groovy graphdb

groovy graphdb

This slidedeck looks at using 7 graph databases and 3 GraphQL libraries using a common case study. The databases covered include: Apache AGE, Apache HugeGraph, Apache TinkerPop, ArcadeDB, Neo4j, OreintDB, and TuGraph. The GraphQL libraries include graphql-java, gql, and neo4j-graphql-java. Sample code uses Apache Groovy but any JVM language will be able to access these databases.

paulking

April 29, 2025
Tweet

More Decks by paulking

Other Decks in Technology

Transcript

  1. Using Graph Databases with Groovy Dr Paul King, VP Apache

    Groovy & Distinguished Engineer Object Computing X | Mastodon: BlueSky | LinkedIn: Apache Groovy: Repo: Slides: Blog: @paulk_asert | @[email protected] @paulk-asert.bsky.social | linkedin.com/in/paulwilliamking/ https://groovy.apache.org/ https://groovy-lang.org/ https://github.com/paulk-asert/groovy-graphdb https://speakerdeck.com/paulk/groovy-graphdb https://groovy.apache.org/blog/groovy-graph-databases
  2. What is Groovy? • A flexible and extensible Java-like language

    for the JVM • Java-like feel and syntax, but with added productivity features • Developed since 2003 because Java (at the time) was * not extensible enough * not succinct enough for scripting * missing cool features from Smalltalk, Python, Ruby, …
  3. Why use Groovy in 2025? • Top 5 Groovy features

    not in Java: • Extension methods • Operator overloading • AST transforms • Dynamic features • Extensible type checking • Top 5 Groovy improvements to Java: • Better OO features • Better functional features • Records • Switch expressions • Java features earlier
  4. AST Transformations // imports not shown public class Book {

    private String $to$string; private int $hash$code; private final List<String> authors; private final String title; private final Date publicationDate; private static final java.util.Comparator this$TitleComparator; private static final java.util.Comparator this$PublicationDateComparator; public Book(List<String> authors, String title, Date publicationDate) { if (authors == null) { this.authors = null; } else { if (authors instanceof Cloneable) { List<String> authorsCopy = (List<String>) ((ArrayList<?>) authors).clone(); this.authors = (List<String>) (authorsCopy instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Set ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Map ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof List ? DefaultGroovyMethods.asImmutable(authorsCopy) : DefaultGroovyMethods.asImmutable(authorsCopy)); } else { this.authors = (List<String>) (authors instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Set ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Map ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof List ? DefaultGroovyMethods.asImmutable(authors) : DefaultGroovyMethods.asImmutable(authors)); } } this.title = title; if (publicationDate == null) { this.publicationDate = null; } else { this.publicationDate = (Date) publicationDate.clone(); } } public Book(Map args) { if ( args == null) { args = new HashMap(); } ImmutableASTTransformation.checkPropNames(this, args); if (args.containsKey("authors")) { if ( args.get("authors") == null) { this .authors = null; } else { if (args.get("authors") instanceof Cloneable) { List<String> authorsCopy = (List<String>) ((ArrayList<?>) args.get("authors")).clone(); this.authors = (List<String>) (authorsCopy instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Set ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Map ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof List ? DefaultGroovyMethods.asImmutable(authorsCopy) : DefaultGroovyMethods.asImmutable(authorsCopy)); } else { List<String> authors = (List<String>) args.get("authors"); this.authors = (List<String>) (authors instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Set ? DefaultGroovyMethods.asImmutable(authors) public Book() { this (new HashMap()); } public int compareTo(Book other) { if (this == other) { return 0; } Integer value = 0 value = this .title <=> other .title if ( value != 0) { return value } value = this .publicationDate <=> other .publicationDate if ( value != 0) { return value } return 0 } public static Comparator comparatorByTitle() { return this$TitleComparator; } public static Comparator comparatorByPublicationDate() { return this$PublicationDateComparator; } public String toString() { StringBuilder _result = new StringBuilder(); boolean $toStringFirst = true; _result.append("Book("); if ($toStringFirst) { $toStringFirst = false; } else { _result.append(", "); } _result.append(InvokerHelper.toString(this.getAuthors())); if ($toStringFirst) { $toStringFirst = false; } else { _result.append(", "); } _result.append(InvokerHelper.toString(this.getTitle())); if ($toStringFirst) { $toStringFirst = false; } else { _result.append(", "); } _result.append(InvokerHelper.toString(this.getPublicationDate())); _result.append(")"); if ($to$string == null) { $to$string = _result.toString(); } return $to$string; } public int hashCode() { if ( $hash$code == 0) { int _result = HashCodeHelper.initHash(); if (!(this.getAuthors().equals(this))) { _result = HashCodeHelper.updateHash(_result, this.getAuthors()); } if (!(this.getTitle().equals(this))) { _result = HashCodeHelper.updateHash(_result, this.getTitle()); } if (!(this.getPublicationDate().equals(this))) { _result = HashCodeHelper.updateHash(_result, this.getPublicationDate()); } $hash$code = (int) _result; } return $hash$code; } public boolean canEqual(Object other) { return other instanceof Book; } public boolean equals(Object other) { if ( other == null) { return false; } if (this == other) { return true; } if (!( other instanceof Book)) { return false; } Book otherTyped = (Book) other; if (!(otherTyped.canEqual( this ))) { return false; } if (!(this.getAuthors() == otherTyped.getAuthors())) { return false; } if (!(this.getTitle().equals(otherTyped.getTitle()))) { return false; } if (!(this.getPublicationDate().equals(otherTyped.getPublicationDate())) ) { return false; } return true; } public final Book copyWith(Map map) { if (map == null || map.size() == 0) { return this; } Boolean dirty = false; HashMap construct = new HashMap(); if (map.containsKey("authors")) { Object newValue = map.get("authors"); Object oldValue = this.getAuthors(); if (newValue != oldValue) { oldValue = newValue; dirty = true; } construct.put("authors", oldValue); } else { construct.put("authors", this.getAuthors()); } if (map.containsKey("title")) { Object newValue = map.get("title"); Object oldValue = this.getTitle(); if (newValue != oldValue) { oldValue = newValue; dirty = true; } construct.put("title", oldValue); } else { construct.put("title", this.getTitle()); } if (map.containsKey("publicationDate")) { Object newValue = map.get("publicationDate"); Object oldValue = this.getPublicationDate(); if (newValue != oldValue) { oldValue = newValue; dirty = true; } construct.put("publicationDate", oldValue); } else { construct.put("publicationDate", this.getPublicationDate()); } return dirty == true ? new Book(construct) : this; } public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(authors); out.writeObject(title); out.writeObject(publicationDate); } public void readExternal(ObjectInput oin) throws IOException, ClassNotFoundException { authors = (List) oin.readObject(); title = (String) oin.readObject(); static { this$TitleComparator = new Book$TitleComparator(); this$PublicationDateComparator = new Book$PublicationDateComparator(); } public String getAuthors(int index) { return authors.get(index); } public List<String> getAuthors() { return authors; } public final String getTitle() { return title; } public final Date getPublicationDate() { if (publicationDate == null) { return publicationDate; } else { return (Date) publicationDate.clone(); } } public int compare(java.lang.Object param0, java.lang.Object param1) { return -1; } private static class Book$TitleComparator extends AbstractComparator<Book> { public Book$TitleComparator() { } public int compare(Book arg0, Book arg1) { if (arg0 == arg1) { return 0; } if (arg0 != null && arg1 == null) { return -1; } if (arg0 == null && arg1 != null) { return 1; } return arg0.title <=> arg1.title; } public int compare(java.lang.Object param0, java.lang.Object param1) { return -1; } } private static class Book$PublicationDateComparator extends AbstractComparator<Book> { public Book$PublicationDateComparator() { } public int compare(Book arg0, Book arg1) { if ( arg0 == arg1 ) { return 0; } if ( arg0 != null && arg1 == null) { return -1; } if ( arg0 == null && arg1 != null) { return 1; } return arg0 .publicationDate <=> arg1 .publicationDate; } public int compare(java.lang.Object param0, java.lang.Object param1) { return -1; } @Immutable(copyWith = true) @Sortable(excludes = 'authors') @AutoExternalize class Book { @IndexedProperty List<String> authors String title Date publicationDate } 10 lines of Groovy or 600 lines of Java
  5. Matrix DSL + Extensible Console jshell> import org.apache.commons.math3.linear.MatrixUtils jshell> double[][]

    d1 = { {10d, 0d}, {0d, 10d}} d1 ==> double[2][] { double[2] { 10.0, 0.0 }, double[2] { 0.0, 10.0 } } jshell> var m1 = MatrixUtils.createRealMatrix(d1) m1 ==> Array2DRowRealMatrix{{10.0,0.0},{0.0,10.0}} jshell> double[][] d2 = { {-1d, 1d}, {1d, -1d}} d2 ==> double[2][] { double[2] { -1.0, 1.0 }, double[2] { 1.0, -1.0 } } jshell> var m2 = MatrixUtils.createRealMatrix(d2) m2 ==> Array2DRowRealMatrix{{-1.0,1.0},{1.0,-1.0}} jshell> System.out.println(m1.multiply(m2.power(2))) Array2DRowRealMatrix{{20.0,-20.0},{-20.0,20.0}}
  6. Strongly-Typed Roman Numeral DSL assert XII + IX == XXI

    assert [LVII + LVII, V * III, IV..(V+I)] == [ CXIV, XV, IV..VI] assert switch(L) { case L -> '50 exactly' case XLV..<L -> 'just below 50' case ~/LI{1,3}/ -> 'just above 50' default -> 'not close to 50' } == '50 exactly’ assert [X, IX, IV, V, VI].sort() == [iv, v, vi, ix, x] assert MMMCMXCIX + I == MMMM [Static type checking] – Not a valid roman numeral: MMMM @ line 15, column 25. assert MMMCMXCIX + I == MMMM ^ com.github.fracpete:romannumerals4j:0.0.1
  7. Graph Database Case Study Women’s 100m Backstroke from 2021 and

    2024 Olympics • Olympic record was broken 7 times across the 2 games
  8. Why Graph Databases? Cypher query and equivalent SQL • Successful

    countries in Paris 2024 SELECT DISTINCT country FROM Swimmer LEFT JOIN Swimmer_Swim ON Swimmer.swimmerId = Swimmer_Swim.fkSwimmer LEFT JOIN Swim ON Swim.swimId = Swimmer_Swim.fkSwim WHERE Swim.at = 'Paris 2024' MATCH (sr:Swimmer)-[:swam]->(sm:Swim {at: 'Paris 2024'}) RETURN DISTINCT sr.country AS country
  9. • All records since London 2012 Why Graph Databases? Cypher

    vs SQL WITH RECURSIVE traversed(swimId) AS ( SELECT fkNew FROM Supersedes WHERE fkOld IN ( SELECT swimId FROM Swim WHERE event = 'Heat 4' AND at = 'London 2012' ) UNION ALL SELECT Supersedes.fkNew as swimId FROM traversed as t JOIN Supersedes ON t.swimId = Supersedes.fkOld WHERE t.swimId = swimId ) SELECT at, event FROM Swim WHERE swimId IN (SELECT * FROM traversed) MATCH (s1:Swim)-[:supersedes*1..10]->(s2:Swim {at: 'London 2012'}) RETURN s1.at as at, s1.event as event
  10. Graph Database Case Study Databases explored • Apache TinkerPop •

    Neo4j • OrientDB • ArcadeDB • Apache AGE • Apache HugeGraph • TuGraph Technologies explored: • GraphQL: graphql-java, gql, neo4j-graphql-java
  11. Graph Database Case Study Databases explored Technologies explored: Gremlin Cypher

    Other features Apache TinkerPop ✓  TinkerPop/Gremlin is widely supported by more than two dozen commercial and open-source graph databases. TinkerGraph is embedded database. Neo4j  ✓ Widely adopted. Cypher & Bolt protocol. Optional neo4j-graphql-java module. OrientDB ✓  Multi-query: SQL with graph extensions, Gremlin Multi-model: Graph, Document, Reactive, Full-Text, & Geospatial ArcadeDB ✓ ✓ Multi-query: SQL with graph extensions, Cypher, Gremlin, MongoDB Query Language Multi-model: Graph, Document, Key/Value, Search-Engine, Time-Series, & Vector-Embedding Apache AGE  ✓ A PostgreSQL extension that provides graph database functionality Apache HugeGraph ✓  Fast import performance even with 10+ billion Vertices and Edges Queries: Gremlin + RESTful API, OLTP, OLAP TuGraph  ✓ A high-performance graph database that has been rigorously tested in Ant Group's 500,000-core graph computing cluster (AliPay). Cypher queries & Bolt protocol. graphql-java Java “engine” supporting GraphQL. Often used with Spring-for-GraphQL but just used bare here. gql GQL is a set of Groovy DSLs and AST transformations built on top of graphql-java to make it easier to build GraphQL schemas and execute GraphQL queries without losing type safety.
  12. Graph Database Case Study jobs: execute: runs-on: ubuntu-latest services: tugraph:

    image: tugraph/tugraph-runtime-centos7:4.5.1 ports: - 8000:8000 - 7687:7687 - 9090:9090 steps: - uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 check-latest: true - uses: gradle/actions/setup-gradle@v4 - name: Run scripts with Gradle run: ./gradlew tugraph:runTS timeout-minutes: 60
  13. • A glimpse at TinkerPop code: define and use two

    vertices, one edge Graph Database Case Study: TinkerPop var name = es.value('name') var country = es.value('country') var at = swim1.value('at') var event = swim1.value('event') var time = swim1.value('time') println "$name from $country swam a time of $time in $event at the $at Olympics" var es = g.addV('Swimmer').property(name: 'Emily Seebohm', country: ' ').next() swim1 = g.addV('Swim').property(at: 'London 2012', event: 'Heat 4', time: 58.23, result: 'First').next() es.addEdge('swam', swim1) Emily Seebohm from swam a time of 58.23 in Heat 4 at the London 2012 Olympics
  14. • TinkerPop offers some minor syntactic sugar using Groovy metaprogramming

    Graph Database Case Study: TinkerPop var name = es.value('name') var country = es.value('country') var at = swim1.value('at') var event = swim1.value('event') var time = swim1.value('time') println "$name from $country swam a time of $time in $event at the $at Olympics" assert g.V().values('result').is(eq(' ')).count().next() == 2 SugarLoader.load() println "$es.name from $es.country swam a time of $swim1.time in $swim1.event at the $swim1.at Olympics" assert g.V.result.is(eq(' ')).count.next() == 2
  15. Graph Database Case Study: Neo4j var name = es.getProperty('name') var

    country = es.getProperty('country') var at = swim1.getProperty('at') var event = swim1.getProperty('event') var time = swim1.getProperty('time') println "$name from $country swam a time of $time in $event at the $at Olympics" es = tx.createNode(label('Swimmer')) es.setProperty('name', 'Emily Seebohm') es.setProperty('country', ' ') swim1 = tx.createNode(label('Swim')) swim1.setProperty('event', 'Heat 4') swim1.setProperty('at', 'London 2012') swim1.setProperty('result', 'First') swim1.setProperty('time', 58.23d) es.createRelationshipTo(swim1, swam) • A glimpse at Neo4j code: define and use two vertices, one edge
  16. Graph Database Case Study: Neo4j es = tx.createNode('Swimmer') es.name =

    'Emily Seebohm' es.country = ' ' swim1 = tx.createNode('Swim') swim1.event = 'Heat 4' swim1.at = 'London 2012' swim1.result = 'First' swim1.time = 58.23d es.swam(swim1) println "$es.name from $es.country swam a time of $swim1.time in $swim1.event at the $swim1.at Olympics" • We can roll our own syntactic sugar for Neo4j using Groovy metaprogramming
  17. • Apache AGE: predefine vertex labels (types) • Apache HugeGraph:

    predefine a schema including vertex labels, edge labels, property names and types, and indexes for certain traversals • Apache TinkerPop: none • ArcadeDB: predefine vertex and edge types • Neo4j: predefine relationships as enums • OrientDB: predefine vertex and edge classes (types) • TuGraph: predefine vertex and edge labels including property names and types and edge from/to types Graph Database Case Study: Setup
  18. Query comparison Case Study has five or six queries examined

    across all databases. We’ll look at two here: • What were the times for Olympic records set in finals? • At which Olympics were records set in heats?
  19. • Cypher queries are embedded within normal SQL queries Query

    comparison: Apache AGE assert sql.rows(''' SELECT * from cypher('swimming_graph', $$ MATCH (s:Swim) WHERE left(s.event, 4) = 'Heat' RETURN s $$) AS (a agtype) ''').a*.map*.get('properties')*.at.toUnique() == ['London 2012', 'Tokyo 2021'] assert sql.rows(''' SELECT * from cypher('swimming_graph', $$ MATCH (s1:Swim {event: 'Final'})-[:supersedes]->(s2:Swim) RETURN s1 $$) AS (a agtype) ''').a*.map*.get('properties')*.time == [57.47, 57.33]
  20. • We send queries to gremlin engine as strings Query

    comparison: Apache HugeGraph var recordSetInHeat = gremlin.gremlin(''' g.V().hasLabel('Swim') .filter(values('event').is(Text.contains('Heat'))) .values('at').dedup().order() ''').execute() assert recordSetInHeat.data() == ['London 2012', 'Tokyo 2021'] var recordTimesInFinals = gremlin.gremlin(''' g.V().has('Swim', 'event', 'Final').as('ev’) .out('supersedes').select('ev').values('time').order() ''').execute() assert recordTimesInFinals.data() == [57.33, 57.47]
  21. • Use gremlin syntax directly (with or without sugar) Query

    comparison: Apache TinkerPop var recordSetInHeat = g.V().has('Swim','event', startingWith('Heat')).values('at').toSet() assert recordSetInHeat == ['London 2012', 'Tokyo 2021'] as Set var recordTimesInFinals = g.V().has('event', 'Final').as('ev').out('supersedes') .select('ev').values('time').toSet() assert recordTimesInFinals == [57.47, 57.33] as Set var recordSetInHeat = g.V.has('Swim','event', startingWith('Heat')).at.toSet assert recordSetInHeat == ['London 2012', 'Tokyo 2021'] as Set var recordTimesInFinals = g.V.has('event', 'Final').as('ev').out('supersedes').select('ev').time.toSet assert recordTimesInFinals == [57.47, 57.33] as Set
  22. • Supports SQL, gremlin, or cypher queries Query comparison: ArcadeDB

    var results = db.query('SQL', ''' SELECT expand(outV()) FROM (SELECT expand(outE('supersedes')) FROM Swim WHERE event = 'Final') ''') assert results*.toMap().time.toSet() == [57.47, 57.33] as Set results = db.query('gremlin', ''' g.V().has('event', 'Final').as('ev').out('supersedes').select('ev').values('time') ''') assert results*.toMap().result.toSet() == [57.47, 57.33] as Set results = db.query('cypher', ''' MATCH (s1:Swim {event: 'Final'})-[:supersedes]->(s2:Swim) RETURN s1.time AS time ''') assert results*.toMap().time.toSet() == [57.47, 57.33] as Set results = db.query('SQL', "SELECT expand(outV()) FROM (SELECT expand(outE('supersedes')) FROM Swim WHERE event.left(4) = 'Heat')") assert results*.toMap().at.toSet() == ['Tokyo 2021', 'London 2012'] as Set
  23. • In our case study, we take advantage of the

    fact we have in-memory variables of the nodes of interest Query comparison: Neo4j var recordSetInHeat = swims.findAll { swim -> swim.event.startsWith('Heat') }*.at assert recordSetInHeat.unique() == ['London 2012', 'Tokyo 2021'] var recordTimesInFinals = swims.findAll { swim -> swim.event == 'Final' && swim.hasRelationship(supersedes) }*.time assert recordTimesInFinals == [57.47d, 57.33d]
  24. • Uses a SQL dialect with graph extensions Query comparison:

    OrientDB var results = db.query("SELECT expand(out('supersedes').in('supersedes')) FROM Swim WHERE event = 'Final'") assert results*.getProperty('time').toSet() == [57.47, 57.33] as Set results = db.query("SELECT expand(out('supersedes')) FROM Swim WHERE event.left(4) = 'Heat'") assert results*.getProperty('at').toSet() == ['Tokyo 2021', 'London 2012'] as Set
  25. • Send queries as strings to server via Neo4j bolt

    client Query comparison: TuGraph assert run(''' MATCH (s:Swim) WHERE s.event STARTS WITH 'Heat' RETURN DISTINCT s.at AS at ''')*.get('at')*.asString().toSet() == ["London 2012", "Tokyo 2021"] as Set assert run(''' MATCH (s1:Swim {event: 'Final'})-[:supersedes]->(s2:Swim) RETURN s1.time as time ''')*.get('time')*.asDouble().toSet() == [57.47d, 57.33d] as Set
  26. GraphQL comparison: graphql-java type Swimmer { name: String! country: String!

    } type Swim { who: Swimmer! at: String! result: String! event: String! time: Float } type Query { findSwim(name: String!, event: String!, at: String!): Swim! findSwims(event: String!): [Swim!] recordsInFinals: [Swim!] recordsInHeats: [Swim!] allRecords: [Swim!] success(at: String!): [Swim!] } • Typical GraphQL scheme definition
  27. • We wire/bind code which fetches our data (records here)

    for each GraphQL query GraphQL comparison: graphql-java record Swimmer(String name, String country) {} record Swim(Swimmer who, String at, String result, String event, double time) {} var es = new Swimmer('Emily Seebohm', ' ') var swim1 = new Swim(es, 'London 2012', 'First', 'Heat 4', 58.23) … var heatsFetcher = { DataFetchingEnvironment env -> swims.findAll{ s -> s.event.startsWith('Heat') && (supersedes[0][1] == s || supersedes.any{ it[0] == s }) } } as DataFetcher<List<Swim>> … builder.dataFetcher("recordsInHeats", heatsFetcher) assert execute('''{ recordsInHeats { at } }''').data.recordsInHeats*.at.toUnique() == ['London 2012', 'Tokyo 2021']
  28. GraphQL comparison: graphql-java var successFetcher = { DataFetchingEnvironment env ->

    var at = env.arguments.at swims.findAll{ s -> s.at == at } } as DataFetcher<List<Swim>> … builder.dataFetcher("success", successFetcher) assert execute(''' query success($at: String!) { success(at: $at) { who { country } } } ''', [at: 'Paris 2024']).data.success*.who*.country.toUnique() == [' ', ' '] • Queries can be parameterized
  29. GraphQL comparison: gql var schema = DSL.schema { queries {

    … field('recordsInHeats') { type list(swimType) fetcher { DataFetchingEnvironment env -> swims.findAll{ s -> s.event.startsWith('Heat') && (supersedes[0][1] == s || supersedes.any{ it[0] == s }) } } } field('success') { type list(swimmerType) argument 'at', GraphQLString fetcher { DataFetchingEnvironment env -> swims.findAll{ s -> s.at == env.arguments.at }*.who } } … • Avoids the need for a String schema
  30. GraphQL comparison: gql DSL.execute(schema, ''' query findSwim($name: String!, $at: String!,

    $event: String!) { findSwim(name: $name, at: $at, event: $event) { who { name country } event at time } } ''', [name: 'Emily Seebohm', at: 'London 2012', event: 'Heat 4']).data .findSwim.with { println "$who.name from $who.country swam a time of $time in $event at the $at Olympics" } • Supports parameterized query definition
  31. GraphQL comparison: gql assert DSL.newExecutor(schema).execute { query('recordsInHeats') { returns(Swim) {

    at } } }.data.recordsInHeats*.at.toUnique() == ['London 2012', 'Tokyo 2021'] var query = DSL.buildQuery { query('success', [at: 'Paris 2024']) { returns(Swimmer) { country } } } assert DSL.execute(schema, query).data.success*.country.toUnique() == [' ', ' '] • Even lets you build queries to avoid “queries as typeless strings”
  32. • Special annotated schema definition support GraphQL comparison: Neo4j var

    schema = ''' type Swimmer { name: String! country: String! } type Swim { who: Swimmer! @relation(name: "swam", direction: IN) at: String! result: String! event: String! time: Float } type Query { success(at: String!): [Swim!] } '''
  33. GraphQL comparison: Neo4j var graphql = new Translator(SchemaBuilder.buildSchema(schema)) var cypher

    = graphql.translate(''' query success($at: String!) { success(at: $at) { who { country } } } ''', [at: 'Paris 2024']) var (q, p) = [cypher.query.first(), cypher.params.first()] assert tx.execute(q, p)*.success*.who*.country.toUnique() == [' ', ' '] • The wiring/binding is automated:
  34. Using Graph Databases with Groovy Dr Paul King, VP Apache

    Groovy & Distinguished Engineer Object Computing X | Mastodon: BlueSky | LinkedIn: Apache Groovy: Repo: Slides: Blog: @paulk_asert | @[email protected] @paulk-asert.bsky.social | linkedin.com/in/paulwilliamking/ https://groovy.apache.org/ https://groovy-lang.org/ https://github.com/paulk-asert/groovy-graphdb https://speakerdeck.com/paulk/groovy-graphdb https://groovy.apache.org/blog/groovy-graph-databases Questions?