Part 2 - CRUD operations & query by example
Following the previous part, in our Spring Data demo, where we built our setup, we will now take a look at CRUD operations and the query by example API.
package com.arangodb.spring.demo.repository; import com.arangodb.spring.demo.entity.Character; import com.arangodb.springframework.repository.ArangoRepository; public interface CharacterRepository extends ArangoRepository{ }
Create a CommandLineRunner
To run our demo with Spring Boot we have to create a class implementing CommandLineRunner
. In this class we can use the @Authowired
annotation to inject our CharacterRepository
– we created one step earlier – and also ArangoOperations
which offers a central support for interactions with the database over a rich feature set. It mostly offers the features from the ArangoDB Java driver with additional exception translation.
To get the injection successfully running we have to add @ComponentScan
to our runner to define where Spring can find our configuration class DemoConfiguration
.
package com.arangodb.spring.demo.runner; import com.arangodb.spring.demo.repository.CharacterRepository; import com.arangodb.springframework.core.ArangoOperations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.ComponentScan; @ComponentScan("com.arangodb.spring.demo") public class CrudRunner implements CommandLineRunner { @Autowired private ArangoOperations operations; @Autowired private CharacterRepository repository; @Override public void run(final String... args) throws Exception { } }
Save and read an entity
It’s time to save our first entity in the database. Both the database and the collection don’t have to be created manually. This happens automatically as soon as we execute a database request with the components involved. We don’t have to leave the Java world to manage our database.
After we saved a character in the database the id in the original entity is updated with the one generated from the database. We can then use this id to find our persisted entity.
package com.arangodb.spring.demo.runner; import com.arangodb.spring.demo.entity.Character; import com.arangodb.spring.demo.repository.CharacterRepository; import com.arangodb.springframework.core.ArangoOperations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.ComponentScan; import java.util.Optional; @ComponentScan("com.arangodb.spring.demo") public class CrudRunner implements CommandLineRunner { @Autowired private ArangoOperations operations; @Autowired private CharacterRepository repository; @Override public void run(String... args) throws Exception { // first drop the database so that we can run this multiple times with the same dataset operations.dropDatabase(); // save a single entity in the database // there is no need of creating the collection first. This happen automatically final Character nedStark = new Character("Ned", "Stark", true, 41); repository.save(nedStark); // the generated id from the database is set in the original entity System.out.println(String.format("Ned Stark saved in the database with id: '%s'", nedStark.getId())); // lets take a look whether we can find Ned Stark in the database final OptionalfoundNed = repository.findById(nedStark.getId()); assert foundNed.isPresent(); System.out.println(String.format("Found %s", foundNed.get())); } }
Run the demo
The last thing we have to do before we can successfully run our demo application is to add our command line runner CrudRunner
in the list of runners in our main class DemoApplication
.
Class>[]runner = new Class>[]{CrudRunner.class};
After executing the demo application we should see the following lines within our console output. The id will of course deviate.
Ned Stark saved in the database with id: '346' Found Character [id=346, name=Ned, surname=Stark, alive=true, age=41]
Update an entity
As everyone probably knows, Ned Stark died in the first season of Game of Thrones. So we have to update his ‘alive’ flag. Thanks to our id
field in the class Character
, we can use the method save()
from our repository to perform an upsert with our variable nedStark
in which id
is already set.
Let’s add the following lines of code to the end of our run()
method in CrudRunner
.
nedStark.setAlive(false); repository.save(nedStark); final OptionaldeadNed = repository.findById(nedStark.getId()); assert deadNed.isPresent(); System.out.println(String.format("The 'alive' flag of the persisted Ned Stark is now '%s'",deadNed.get().isAlive()));
If we run our demo a second time the console output should look like this:
Ned Stark saved in the database with id: '508' Found Character [id=508, name=Ned, surname=Stark, alive=true, age=41] The 'alive' flag of the persisted Ned Stark is now 'false'
Save and read multiple entities
What we can do with a single entity, we can also do with multiple entities. It’s not just a single method call for convenience purpose, it also requires only one database request.
Let’s save a bunch of characters. Only main casts of Game of Thrones – but that’s already a lot. After that we fetch all of them from our collection and count them.
Extend the run()
method with these lines of code.
CollectioncreateCharacters = createCharacters(); System.out.println(String.format("Save %s additional chracters",createCharacters.size())); repository.saveAll(createCharacters); Iterable all = repository.findAll(); long count=StreamSupport.stream(Spliterators.spliteratorUnknownSize(all.iterator(),0),false).count(); System.out.println(String.format("A total of %s characters are persisted in the database",count));
We also need the method createCharacters()
which looks as follow:
public static CollectioncreateCharacters(){ return Arrays.asList(new Character("Robert","Baratheon",false), new Character("Jaime","Lannister",true,36),new Character("Catelyn","Stark",false,40), new Character("Cersei","Lannister",true,36),new Character("Daenerys","Targaryen",true,16), new Character("Jorah","Mormont",false),new Character("Petyr","Baelish",false), new Character("Viserys","Targaryen",false),new Character("Jon","Snow",true,16), new Character("Sansa","Stark",true,13),new Character("Arya","Stark",true,11), new Character("Robb","Stark",false),new Character("Theon","Greyjoy",true,16), new Character("Bran","Stark",true,10),new Character("Joffrey","Baratheon",false,19), new Character("Sandor","Clegane",true),new Character("Tyrion","Lannister",true,32), new Character("Khal","Drogo",false),new Character("Tywin","Lannister",false), new Character("Davos","Seaworth",true,49),new Character("Samwell","Tarly",true,17), new Character("Stannis","Baratheon",false),new Character("Melisandre",null,true), new Character("Margaery","Tyrell",false),new Character("Jeor","Mormont",false), new Character("Bronn",null,true),new Character("Varys",null,true),new Character("Shae",null,false), new Character("Talisa","Maegyr",false),new Character("Gendry",null,false), new Character("Ygritte",null,false),new Character("Tormund","Giantsbane",true), new Character("Gilly",null,true),new Character("Brienne","Tarth",true,32), new Character("Ramsay","Bolton",true),new Character("Ellaria","Sand",true), new Character("Daario","Naharis",true),new Character("Missandei",null,true), new Character("Tommen","Baratheon",true),new Character("Jaqen","H'ghar",true), new Character("Roose","Bolton",true),new Character("The High Sparrow",null,true)); }
After executing the demo again the console should print the following additional lines:
Save 42 additional chracters A total of 43 characters are persisted in the database
Counting is just an example of what you can do with the returned entities and it’s also not a perfect one. Fetching every entity from a collection only to count them is quite inefficient. As an alternative we can use the method count()
from ArangoRepository
or we use ArangoOperations
for it.
// count with ArangoRepository long count = repository.count(); // count with ArangoOperations long count = operations.collection(Character.class).count();
Read with sorting and paging
Next to the normal findAll()
, ArangoRepository
also offers the ability to sort the fetched entities by a given field name. Adding the following source code at the end of your run()
method gives you all characters sorted by field name
.
System.out.println("## Return all characters sorted by name"); IterableallSorted = repository.findAll(Sort.by(Sort.Direction.ASC, "name")); allSorted.forEach(System.out::println);
Furthermore, it’s possible to use pagination combined with sorting. With the following code you get the first 5 characters sorted by name.
System.out.println("## Return the first 5 characters sorted by name"); Pagefirst5Sorted = repository.findAll(PageRequest.of(0, 5, Sort.by(Sort.Direction.ASC, "name"))); <first5Sorted.forEach(System.out::println);<
Your console output should include Arya Stark, Bran Stark, Brienne Tarth, Bronn and Catelyn Stark.
## Return the first 5 characters sorted by name Character [id=1898, name=Arya, surname=Stark, alive=true, age=11] Character [id=1901, name=Bran, surname=Stark, alive=true, age=10] Character [id=1921, name=Brienne, surname=Tarth, alive=true, age=32] Character [id=1913, name=Bronn, surname=null, alive=true, age=null] Character [id=1890, name=Catelyn, surname=Stark, alive=false, age=40]
Query by example
Since version 1.12 Spring Data provides the interface QueryByExampleExecutor
which is also supported by ArangoDB Spring Data. It allows execution of queries by Example
instances.
Lets create a new CommandLineRunner
for this
package com.arangodb.spring.demo.runner; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.ComponentScan; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import com.arangodb.spring.demo.entity.Character; import com.arangodb.spring.demo.repository.CharacterRepository; @ComponentScan("com.arangodb.spring.demo") public class ByExampleRunner implements CommandLineRunner { @Autowired private CharacterRepository repository; @Override public void run(final String... args) throws Exception { System.out.println("# Query by example"); } }
and add it to our DemoApplication
.
Class>[]runner = new Class>[]{ CrudRunner.class, ByExampleRunner.class };
Single entity
First we want to find Ned Stark again. But this time we don’t know the id of the persisted entity. We start with creating a Character with the same property values as the searched one. Then create an Example
instance of it with Example.of(T)
and search for it with findOne(Example)
from our CharacterRepository
.
final Character nedStark=new Character("Ned", "Stark", false, 41); System.out.println(String.format("## Find character which exactly match %s",nedStark)); OptionalfoundNedStark = repository.findOne(Example.of(nedStark)); assert foundNedStark.isPresent(); System.out.println(String.format("Found %s", foundNedStark.get()));
If we did everything right the console output should be:
# Query by example ## Find character which exactly match Character [id=null, name=Ned, surname=Stark, alive=false, age=41] Found Character [id=1880, name=Ned, surname=Stark, alive=false, age=41]
Multiple entities
Now we want to find more than one entity. During the time when I watched Game of Thrones I saw a lot of Starks dying, so lets take look who is already dead. For this we create a new instance of Character with surname
‘Stark’ and alive
false. Because we only need these two fields in our entity, we have to ignore the other fields in our ExampleMatcher.
System.out.println("## Find all dead Starks"); IterableallDeadStarks = repository .findAll(Example.of(new Character(null, "Stark", false),ExampleMatcher.matchingAll() .withMatcher("surname", GenericPropertyMatcher::exact).withIgnorePaths("name", "age"))); allDeadStarks.forEach(System.out::println);
After executing the application the console output should be:
## Find all dead Starks Character [id=1887, name=Ned, surname=Stark, alive=false, age=41] Character [id=1890, name=Catelyn, surname=Stark, alive=false, age=40] Character [id=1899, name=Robb, surname=Stark, alive=false, age=null]
In addition to search for specific values from our example entity, we can search for dynamically depending values. In the next example we search for a Stark who is 30 years younger than Ned Stark. Instead of changing the age for Ned Stark in the previously fetched entity, we use a transformer within the ExampleMatcher and subtract 30 from the age of Ned Stark.
System.out.println("## Find all Starks which are 30 years younger than Ned Stark"); IterableallYoungerStarks = repository.findAll( Example.of(foundNedStark.get(), ExampleMatcher.matchingAll() .withMatcher("surname", GenericPropertyMatcher::exact) .withIgnorePaths("id", "name", "alive") .withTransformer("age", age -> age.map(it -> (int) it - 30)))); allYoungerStarks.forEach(System.out::println);
Because we are using the entity foundNedStark
– fetched from the database – we have to ignore the field id
which isn’t null in this case.
The console output should only include Arya Stark.
## Find all Starks which are 30 years younger than Ned Stark Character [id=1898, name=Arya, surname=Stark, alive=true, age=11]
Aside from searching for exact and transformed values we can – in case of type String also search for other expressions. In this last case we search for every character whose surname
ends with ‘ark’. The console output should include every Stark.
System.out.println("## Find all character which surname ends with 'ark' (ignore case)"); Iterableark = repository.findAll(Example.of(new Character(null, "ark", false), ExampleMatcher.matchingAll().withMatcher("surname", GenericPropertyMatcher::endsWith) .withIgnoreCase() .withIgnorePaths("name", "alive", "age"))); ark.forEach(System.out::println);
What's next
In the next part of our Spring Data demo we’re going to take a look how we can use custom methods in our repository interfaces to perform AQL queries without the need of writing them on our own.