Using Domain-Specific Language to Manipulate NoSQL Databases in Java With Eclipse JNoSQL
From Wikipedia, "A domain-specific language (DSL) is a computer language specialized to a particular application domain." The DSL has several books, and the most famous one from Martin Fowler says, "DSLs are small languages, focused on a particular aspect of a software system." That is often referred to as a fluent interface. In the NoSQL world, we have an issue, as the picture below shows. We have four different document NoSQL databases doing exactly the same thing, however, with different APIs. Does it make sense to have a standard do these habitual behaviors? In this article, we'll cover who does manipulation with Eclipse JNoSQL API.
Querying NoSQL Database Programmatically in Java
To manipulate any entity in all NoSQL types, there is a template interface. The template offers convenience operations to create, update, delete, and query for NoSQL databases and provides a mapping between your domain objects and JNoSQL. That looks like a template method to NoSQL databases, however, no heritage is necessary. There are DocumentTemplate, ColumnTemplate, GraphTemplate, and KeyValueTemplate.
Key-Value
In the Key-value database, there is a KeyValueTemplate in this NoSQL storage technology. Usually, all the operations are defined by the ID, therefore, it has a smooth query.
KeyValueTemplate template = ...;
User user = User.builder().withPhones(Arrays.asList("234", "432"))
.withUsername("ada").withName("Name").build();
template.put(user);
Optional<User> result = template.get("ada", User.class);
template.remove("ada");
Column-Family
The column family has a little more complex structure, however, the search from the key is still recommended. E.g.: Both Cassandra and HBase have a secondary index, however, neither have a guarantee about performance, and they usually recommend to have a second table whose rowkey is your "secondary index" and is only being used to find the rowkey needed for the actual table. Given Person as an entity and then we would like to operate from the field ID, which is the entity from the Entity.
@Entity("Person")
public class Person {
@Id("id")
private Long id;
@Column
private String name;
@Column
private Integer age;
@Column
private List<String> phones;
}
ColumnTemplate template = ...;
Person ada = Person.builder().withPhones(Arrays.asList("234", "432"))
.withName("Ada Lovelace")
.withId(1L).build();
template.insert(ada);
ColumnQuery select = select().from("Person").where("id").eq(1L).build();
Optional<Person> result = template.singleResult(select);
ColumnDeleteQuery delete = delete().from("Person").where("id").eq(1L).build();
template.delete(delete);
Document Collection
The document types allow more complex queries, so more complex entities, with a document type a developer can find from different fields than id easily and naturally. Also, there is NoSQL documents type that supports aggregations query, however, Eclipse JNoSQL does not support this yet. At the Eclipse JNoSQL API perspective, the document and column type is pretty similar, but with the document, a Java developer might do a query from a field that isn't a key and neither returns an unported operation exception or adds a secondary index for this. So, given the same Person entity with document NoSQL type, a developer can do more with queries, such as "person" between "age."
DocumentTemplate template = ...;
Person person = Person.builder().
withPhones(Arrays.asList("234", "432"))
.withName("Ada")
.withId(id)
.withIgnore("Just Ignore").build();
template.insert(person);
DocumentQuery selectByAge = select().from("Person")
.where("age").between(10, 20).build();
select().from("Person").where("age").lt(10).build();//find lesser than 10
select().from("Person").where("age").lte(21).build();//find lesser equals 21
select().from("Person").where("name").eq("Ada").build();//find equals 'Ada'
Optional<Person> personOptional = template.singleResult(selectByAge);
DocumentDeleteQuery deleteFromName = delete().from("Person").where("name").eq("Ada").build();
template.delete(deleteFromName);
Graph
If an application needs a recommendation engine or a full detail about the relationship between two entities in your system, it requires a graph database. A graph database has the vertex and the edge. The edge is an object that holds the relationship information about the edges and has direction and properties that make it perfect to maps or human relationship. To the Graph API, Eclipse JNoSQL uses the Apache Tinkerpop. Likewise, the GraphTemplate is a wrapper to convert a Java entity to Vertex in TinkerPop.
GraphTemplate graph =
Person banner = graph.insert(builder().withAge(30).withName("Banner")
.withOccupation("Developer").withSalary(3_000D).build());
Person natalia = graph.insert(builder().withAge(32).withName("Natalia")
.withOccupation("Developer").withSalary(5_000D).build());
Person rose = graph.insert(builder().withAge(40).withName("Rose")
.withOccupation("Design").withSalary(1_000D).build());
Person tony = graph.insert(builder().withAge(22).withName("tony")
.withOccupation("Developer").withSalary(4_500D).build());
EdgeEntity knows = graph.edge(tony, "knows", rose);
knows.add("feel", "love");
graph.edge(natalia, "knows", rose);
graph.edge(banner, "knows", rose);
List<Person> developers = graph.getTraversalVertex()
.has("salary", gte(3_000D))
.has("age", between(20, 25))
.has("occupation", "Developer")
.<Person>stream().collect(toList());
List<Person> peopleWhoDeveloperKnows = graph.getTraversalVertex()
.has("salary", gte(3_000D))
.has("age", between(20, 25))
.has("occupation", "Developer")
.out("knows")
.<Person>stream().collect(toList());
List<Person> both = graph.getTraversalVertex()
.has("salary", gte(3_000D))
.has("age", between(20, 25))
.has("occupation", "Developer")
.outE("knows")
.bothV()
.<Person>stream()
.distinct()
.collect(toList());
List<Person> couple = graph.getTraversalVertex()
.has("salary", gte(3_000D))
.has("age", between(20, 25))
.has("occupation", "Developer")
.outE("knows")
.has("feel", "love")
.bothV()
.<Person>stream()
.distinct()
.collect(toList());
We won't see more profound about how the Gremlin works, however, there is an article that covers this topic.
Querying NoSQL Database From a Text (String)
Even with API in Java, sometimes a developer either needs to keep a query in a file to feel more comfortable querying with text. To make that, it was born a subproject in JNoSQL that is Eclipse JNoSQL Aphrodite. This subproject contains the syntax query to the API. Briefly, it is a query implemented by Antlr that does a parse to a programmatically API, then follows the flow to the specific database implementation.
Initially, this query has basic principles:
- All instructions end with a break like `\n`
- It is case sensitive
- All keywords must be in lowercase
- The goal is to look like SQL, however simpler
- Even passing in the syntax and parser the query, a specific implementation may not support an operation. E.g., Column family may not support query in a different field that is not the ID field.
Get more details.
Key-Value
The key-value has three operations, put, remove and get.
Get
Retrieving data for an entity is done using a GET statement:
get_statement ::= GET ID (',' ID)*
//sample
get "Apollo" //to return an element where the id is 'Apollo'
get "Diana" "Artemis" //to return a list of values from the ids
Remove
To delete one or more entities, use the remove statement
del_statement ::= GET ID (',' ID)*
//sample
remove "Apollo"
remove "Diana" "Artemis"
Put
To either insert or override values from a key-value database, use the put statement.
put_statement ::= PUT {KEY, VALUE, [TTL]}
//sample
put {"Diana" , "The goddess of hunt"}//adds key -diana and value ->"The goddess of hunt"
put {"Diana" , "The goddess of hunt", 10 second}//also defines a TTL of 10 seconds
Column and Document
Both have sample syntax that looks like a SQL query, however, remember it has a limitation and does not support joins. Document types are usually more queriable than a column type. They have four operations: insert, update, delete, and select.
Insert
Inserting data for an entity is done using an INSERT statement:
insert_statement ::= INSERT entity_name (name = value, (`,` name = value) *) [ TTL ]
//sample
insert God (name = "Diana", age = 10)
insert God (name = "Diana", age = 10, power = {"sun", "god"})
insert God (name = "Diana", age = 10, power = {"sun", "god"}) 1 day
Update
Updating an entity is done using an update statement:
update_statement ::= UPDATE entity_name (name = value, (`,` name = value) *)
//sample
update God (name = "Diana", age = 10)
update God (name = "Diana", age = 10, power = {"hunt", "moon"})
Delete
Deleting either an entity or fields uses the delete statement
delete_statement ::= DELETE [ simple_selection ( ',' simple_selection ) ]
FROM entity_name
WHERE where_clause
//sample
delete from God
delete name, age ,adress.age from God where id = "Diana"
Select
The select statement reads one or more fields for one or more entities. It returns a result-set of the entities matching the request, where each entity contains the fields for corresponding to the query.
select_statement ::= SELECT ( select_clause | '*' )
FROM entity_name
[ WHERE where_clause ]
[ SKIP (integer) ]
[ LIMIT (integer) ]
[ ORDER BY ordering_clause ]
//select
select * from God
select name, age ,adress.age from God order by name desc age desc
select * from God where birthday between "01-09-1988" and "01-09-1988" and salary = 12
select name, age ,adress.age from God skip 20 limit 10 order by name desc age desc
We are now done with defining the query syntax. The next step is to use this resource at the template interface.
DocumentTemplate documentTemplate = ..;
ColumnTemplate columnTemplate = ...;
KeyValueTempalte keyValueTemplate =...;
GraphTemplate graphTemplate =...;
List<Movie> movies = documentTemplate.query("select * from Movie where year > 2012");
List<Person> people = columnTemplate.query("select * from Person where id = 12");
Optional<God> god = keyValueTemplate.query("get \"Diana\"");
List<City> cities = graphTemplate.query("g.V().hasLabel('City')");
To run a query dynamically, use the prepare method. It will return a PreparedStatement interface. To define a parameter to key-value, document, and column query, use the "@" in front of the name.
PreparedStatement preparedStatement = documentTemplate.prepare("select * from Person where name = @name");
preparedStatement.bind("name", "Ada");
List<Person> adas = preparedStatement.getResultList();
//to graph just keep using gremlin
PreparedStatement prepare = graphTemplate().prepare("g.V().hasLabel(param)");
prepare.bind("param", "Person");
List<Person> people = preparedStatement.getResultList();
Repository
The Repository interface contains all the trivial methods shared among the NoSQL implementations that a developer does not need to care about. Also, there is query method that does a query based on the method name. In the next version came two new annotations: the Query and param that defines the statement and set the values in the query respectively.
interface PersonRepository extends Repository<Person, Long> {
@Query("select * from Person")
Optional<Person> findByQuery();
@Query("select * from Person where id = @id")
Optional<Person> findByQuery(@Param("id") String id);
}
Remember, when a developer defines who that repository will be implemented from the CDI qualifier, the query will be executed to that defined type, given that, gremlin to Graph, JNoSQL key to key-value and so on.
@Inject
@Database(value = DatabaseType.COLUMN)
private PersonRepository repository;
Conclusion
This article covered how a Java developer can do NoSQL operations using just one DSL either programmatically or by text. The goal of this query in Eclipse JNoSQL is to make easier the integration between the Java and a NoSQL database, however, don't forget the specific behavior, even with this resource the framework still working with specific queries such as Cassandra Query Language, Arango Query Language, N1QL, and so on.