Slide 1

Slide 1 text

Using Apache TinkerPop, AGE & HugeGraph (Incubating) 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

Concise Code … private static boolean isInteger(String s) { return s.matches("-?\\d+"); } private static int partitionPoint(List list, Predicate predicate) { for (int i = 0; i < list.size(); i++) { if (!predicate.test(list.get(i))) { return i; } } return list.size(); } } // 10 times slower import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.function.Predicate; import java.util.stream.Collectors; public class SortNumericStrings { private static final Map WORD_NUMBERS = Map.of( "ZERO", 0, "ONE", 1, "TWO", 2, /* ...add more... */ "TEN", 10 ); public static void main(String[] args) { List nums = Arrays.asList("222", "ZERO", "4", "33", "PI", "TWO"); nums.sort(Comparator.comparing(SortNumericStrings::isInteger)); int pp = partitionPoint(nums, s -> !isInteger(s)); nums.subList(0, pp).sort(Comparator.reverseOrder()); nums.subList(pp, nums.size()).sort(Comparator.comparingInt(Integer::parseInt)); if (!nums.equals(List.of("ZERO", "TWO", "PI", "4", "33", "222"))) { throw new AssertionError("Sort mismatch: " + nums); } List result = nums.stream().map(s -> { if (isInteger(s)) { return Integer.parseInt(s); } else if ("PI".equals(s)) { return java.lang.Math.PI; } else if ("E".equals(s)) { return java.lang.Math.E; } else if (WORD_NUMBERS.containsKey(s)) { return WORD_NUMBERS.get(s); } else { throw new IllegalArgumentException("Unknown literal: " + s); } }).collect(Collectors.toList()); List expected = List.of(0, 2, Math.PI, 4, 33, 222); if (!result.equals(expected)) { throw new AssertionError("Transformation mismatch: " + result); } } … var nums = ['222', 'ZERO', '4', '33', 'PI', 'TWO'] assert nums.toSorted() == ['222', '33', '4', 'PI', 'TWO', 'ZERO'] nums.sort(String::isInteger) var pp = nums.partitionPoint{ !it.integer } nums.sort(0.. it.toInteger() case ['PI', 'E'] -> Math."$it" default -> BigInteger."$it" } } == [0, 2, 3.141592653589793, 4, 33, 222]

Slide 4

Slide 4 text

Extension methods • 2000 extension methods to 150+ JDK classes enhance JDK functionality • 350 new methods added in Groovy 5 Better

Slide 5

Slide 5 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 6

Slide 6 text

Domain Specific Languages 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 show the square root of 64 muestra la raíz cuadrada de 64 まず 64 の 平方根 を 表示する 显示 64 的平方根

Slide 7

Slide 7 text

Nice Repl (groovysh)

Slide 8

Slide 8 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 9

Slide 9 text

Graph Database Case Study

Slide 10

Slide 10 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 11

Slide 11 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 12

Slide 12 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 13

Slide 13 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 14

Slide 14 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 15

Slide 15 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 16

Slide 16 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 17

Slide 17 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 18

Slide 18 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 19

Slide 19 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 20

Slide 20 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 21

Slide 21 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 22

Slide 22 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 23

Slide 23 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 24

Slide 24 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 25

Slide 25 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 26

Slide 26 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 27

Slide 27 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 28

Slide 28 text

GraphQL Source: https://graphql.org/

Slide 29

Slide 29 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 30

Slide 30 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 31

Slide 31 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 32

Slide 32 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 33

Slide 33 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 34

Slide 34 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 35

Slide 35 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 36

Slide 36 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 37

Slide 37 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?