Search
ctrl/
Ask AI
Light
Dark
System

Data modeling

The Java driver allows you to structure your query results as classes.

You can simply create a class that matches the schema types’ properties by following the scalar type map.

Java
Schema 2.x
Schema 3+
Copy
@EdgeDBType
public class Person {
    public String name;
    public int Age;
}
Copy
module default {
    type Person {
        property name -> str;
        property age -> int32;
    }
}
Copy
module default {
    type Person {
        name: str;
        age: int32;
    }
}

There are a few requirements with the class representation:

  • All classes that represent data need to be marked with the @EdgeDBType annotation.

  • Any multi-link property (collection) needs to be marked with the @EdgeDBLinkType annotation.

  • A field must be public or have a valid setter if useFieldSetters is true in the client configuration.

If a field cannot be mapped from a value within a result, it is simply ignored. This allows the same Java type to be used for queries with different shapes.

Naming strategies control the map between Java names and schema names. By default, no mutation is applied to the field names of the Java class. This means myFieldName is directly mapped to myFieldName.

Default implementations of NamingStrategy are available as static methods under the interface, for example:

Copy
var config = EdgeDBClientConfig.builder()
    .withNamingStrategy(NamingStrategy.snakeCase())
    .build();

chooses the snake_case naming strategy, which converts any given name to ‘snake_case’.

You can configure whether or not the binding will attempt to use field setters if present with the useFieldSetters configuration option. When this is true, the binding will attempt to find methods in your class that meet the following requirements:

  • Is prefixed with set followed by the field name in PascalCase

  • Contain one parameter with the same type of the field

  • Is public and non-static

For example, creating a bean that represents the Person schema type:

Java
Schema 2.x
Schema 3+
Copy
@EdgeDBType
public class Person {
    private String name;
    private int age;

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return this.name;
    }

    public int getAge() {
        return this.age;
    }
}
Copy
module default {
    type Person {
        property name -> str;
        property age -> int32;
    }
}
Copy
module default {
    type Person {
        name: str;
        age: int32;
    }
}

The driver will give priority to the setName and setAge methods rather than using the reflection API to set the field values.

You can specify a constructor as a target for deserialization with the @EdgeDBDeserializer annotation. A deserializer has 2 valid modes of operation: enumeration consumers or value consumers.

An enumerator consumer takes only one parameter, an ObjectEnumerator interface, which provides a direct handle to the deserialization pipeline. Calling the next() method preforms the deserialization step for one element and returns an ObjectEnumerator.ObjectElement class, containing the name, type, and value.

Copy
@EdgeDBType
public class Person {
    private String name;
    private int age;

    public Person(ObjectEnumerator enumerator) {
        try {
            ObjectEnumerator.ObjectElement element;
            while(enumerator.hasRemaining() && (element = enumerator.next()) != null) {
                switch(element.getName()) {
                    case "name":
                        assert element.getType() == String.class;
                        this.name = (String)element.getValue();
                        break;
                    case "age":
                        assert element.getType() == Integer.class;
                        this.age = (int)element.getValue();
                        break;

                }
            }
        } catch(EdgeDBException err) { // deserialization error

        } catch(OperationNotSupportedException err) { // read/IO error

        }
    }
}

This approach isn’t viable for large data structure maps. Instead, it is useful for other data type representations, like tuples:

Copy
@EdgeDBDeserializer
public SimpleTuple(ObjectEnumerator enumerator)
throws EdgeDBException, OperationNotSupportedException {
    elements = new ArrayList<>();

    while(enumerator.hasRemaining()) {
        var enumerationElement = enumerator.next();

        assert enumerationElement != null;

        elements.add(new Element(
            enumerationElement.getType(),
            enumerationElement.getValue()
        ));
    }
}

Value consumers take in the fields’ values in the constructor, mapped by a @EdgeDBName annotation:

Java
Schema 2.x
Schema 3+
Copy
@EdgeDBType
public class Person {
    private final String name;
    private final int age;

    @EdgeDBDeserializer
    public Person(
        @EdgeDBName("name") String name,
        @EdgeDBName("age") int age
    ) {
        this.name = name;
        this.age = age;
    }
}
Copy
module default {
    type Person {
        property name -> str;
        property age -> int32;
        multi link friends -> Person;
    }
}
Copy
module default {
    type Person {
        name: str;
        age: int32;
        multi friends: Person;
    }
}

The binding supports polymorphic types, allowing you to reflect your abstract schema types in code. For example:

Java
Schema 2.x
Schema 3+
Copy
@EdgeDBType
public abstract class Media {
    public String title;
}

@EdgeDBType
public class Show extends Media {
    public Long seasons;
}

@EdgeDBType
public class Movie extends Media {
    public Long release_year;
}
Copy
module default {
    abstract type Media {
        required property title -> str {
            constraint exclusive;
        }
    }

    type Movie extending Media {
        required property release_year -> int64;
    }

    type Show extending Media {
        required property seasons -> int64;
    }
}
Copy
module default {
    abstract type Media {
        required title: str {
            constraint exclusive;
        }
    }

    type Movie extending Media {
        required release_year: int64;
    }

    type Show extending Media {
        required seasons: int64;
    }
}

With this schema, you can specify Media as a result of a query. The binding will then discover any subclasses of Media and deserialize the subclasses as a result.

Copy
client.query(Media.class, "SELECT Media { title, [IS Movie].release_year, [IS Show].seasons }")
    .thenAccept(result -> {
        for(var media : result) {
            if(media instanceof Show) {
                var show = (Show)media;
                System.out.println(String.format("Got show: %s, %d", show.title, show.seasons));
            } else if (media instanceof Movie) {
                var movie = (Movie)media;
                System.out.println(String.format("Got movie: %s, %d", movie.title, movie.release_year));
            }
        }
    });