Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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, …

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

AST Transformations // imports not shown public class Book { private String $to$string; private int $hash$code; private final List 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 authors, String title, Date publicationDate) { if (authors == null) { this.authors = null; } else { if (authors instanceof Cloneable) { List authorsCopy = (List) ((ArrayList) authors).clone(); this.authors = (List) (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) (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 authorsCopy = (List) ((ArrayList) args.get("authors")).clone(); this.authors = (List) (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 authors = (List) args.get("authors"); this.authors = (List) (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 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 { 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 { 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 authors String title Date publicationDate } 10 lines of Groovy or 600 lines of Java

Slide 5

Slide 5 text

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}}

Slide 6

Slide 6 text

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.. '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

Slide 7

Slide 7 text

Graph Database Case Study Women’s 100m Backstroke from 2021 and 2024 Olympics • Olympic record was broken 7 times across the 2 games

Slide 8

Slide 8 text

Graph Database Case Study

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

• 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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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.

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

• 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

Slide 15

Slide 15 text

• 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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

• 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

Slide 19

Slide 19 text

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?

Slide 20

Slide 20 text

• 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]

Slide 21

Slide 21 text

• Both gremlin and (partially supported?) cypher queries strings to server 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] assert cypher.execute(''' MATCH (s1:Swim {event: 'Final'})-[:supersedes]->(s2:Swim) RETURN s1.time as time ''').data()*.time == [57.47, 57.33]

Slide 22

Slide 22 text

• 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

Slide 23

Slide 23 text

• 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

Slide 24

Slide 24 text

• 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]

Slide 25

Slide 25 text

• 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

Slide 26

Slide 26 text

• 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

Slide 27

Slide 27 text

GraphQL Source: https://graphql.org/

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

• 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> … builder.dataFetcher("recordsInHeats", heatsFetcher) assert execute('''{ recordsInHeats { at } }''').data.recordsInHeats*.at.toUnique() == ['London 2012', 'Tokyo 2021']

Slide 30

Slide 30 text

GraphQL comparison: graphql-java var successFetcher = { DataFetchingEnvironment env -> var at = env.arguments.at swims.findAll{ s -> s.at == at } } as DataFetcher> … 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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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() == [' ', ' '] GraphQL comparison: gql • Even lets you build queries to avoid “queries as typeless strings”

Slide 34

Slide 34 text

• 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!] } '''

Slide 35

Slide 35 text

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:

Slide 36

Slide 36 text

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?