Consider a typical application that interacts with database data. What operations can it perform? First, it needs to create and store data in the database. Then, it needs to read or access that data. After retrieving the data, it might need to update or delete it from the database. These fundamental operations are known as CRUD operations, which stands for Create, Read, Update, and Delete.
This topic will show you how to use CRUD operations to interact with a database in a Spring application. You can apply these concepts to any database that Spring Data supports.
Repositories in Spring
We manipulate database data in Java and Kotlin using an entity. For example, suppose you decide to open a fitness center and want to store all information about your fitness equipment. At this point, you only have a Treadmill. That's not much equipment, right? But you've just started your business, so it will take some time to accumulate more. Now, you need to implement all the CRUD operations for this entity. That could be quite tedious, especially when you eventually end up with dozens of different entities in your gym!
Luckily, Spring data introduces the Repository concept. But what is it, and how can it help us? In Spring Data, a repository is an abstraction that reduces the amount of boilerplate code. Spring Data provides various repository interfaces that are database-agnostic, meaning you can use them with any relational or NoSQL database.
The most basic interface is the Repository:
@Indexed
public interface Repository<T, ID> {
}Notice the @Indexed annotation here? It signifies that all descendants of this interface should be considered as candidates for repository beans. Also, notice that the Repository interface doesn't have any declared methods. That's because it serves as a marker interface.
The Repository interface is generic. The generic type T represents an entity type, while the generic type ID represents the entity's unique ID type.
For CRUD operations, Spring Data offers the CrudRepository interface:
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
// CREATE/UPDATE methods
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
// READ methods
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
// DELETE methods
void deleteById(ID id);
void delete(T entity);
void deleteAllById(Iterable<? extends ID> ids);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}The CrudRepository interface includes operations for each CRUD action. As you see, there are more than four methods: the interface allows you to pass either an entity object or an entity ID to its methods. Also, you can work with individual entities or multiple entities at once.
You might notice that the CrudRepository interface has the same declared methods for the Create and Update operations. Under the hood, Spring data checks whether a given entity is new or old and then, based on that, either creates or updates the entity.
Don't worry if you can't remember all details of these interfaces right away. The source code is always available and easy to find. What matters is understanding the main ideas behind the CrudRepository.
Declaring a repository
Now you're fully equipped to create a repository.
Let's start with the Treadmill class:
Java
@Entity
public class Treadmill {
@Id
private String code;
private String model;
// constructors, getters, setters, toString
}Kotlin
@Entity
class Treadmill(
@Id
var code: String,
var model: String
) {
override fun toString(): String {
return "Treadmill(code='$code', model='$model')"
}
}The way you declare an entity depends on the actual database you're using. Depending on your target database, you might need to choose the @Entity, @Document, @KeySpace, or another annotation.
@Entityis used for JPA (Java Persistence API) entities, typically associated with relational databases.@Documentis used for entities in a document-based NoSQL database like MongoDB.@KeySpaceis used for entities in a keyspace in a Cassandra database.
What an entity maps to depends on the database you're using. It could be a table (as in relational databases), a document (as in NoSQL document-oriented databases like MongoDB), a node or an edge (as in the NoSQL graph database Neo4J), or another type of user-defined structure.
To create our repository, we need to extend CrudRepository and specify the entity type (Treadmill) and the entity ID type (String) as follows:
Java
public interface TreadmillRepository extends CrudRepository<Treadmill, String> {
}Kotlin
interface TreadmillRepository : CrudRepository<Treadmill, String>That's it. In this topic, we're using the String property code as the ID, but you can use any type you want. In real-world applications, people commonly use the Long type, especially when they're using a relational database with sequential numbers as an ID. Spring generates required implementations for all CRUD methods specified in the CrudRepository interface.
Keep in mind that in our examples, we've used a Treadmill entity and a TreadmillRepository. To implement these in your projects, you would need to add database-specific annotations to the Treadmill entity or write getters, setters, constructors, toString() methods, and other methods.
Example setup
From this section onwards, we'll create concrete examples to apply the theory discussed above. First, don't forget to write the following dependencies:
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>Gradle - Groovy
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//...
}Gradle - Kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.h2database:h2")
//...
}We'll use the same entity and its repository as before. Here's how to write an Application runner that prints some logs to the console:
Java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Component
class Runner implements ApplicationRunner {
private final TreadmillRepository repository;
// constructor injection for TreadmillRepository
Runner(TreadmillRepository treadmillRepository) {
this.repository = treadmillRepository;
}
private void doWeHaveSomethingInDb() {
long count = repository.count();
if (count > 0) {
System.out.printf("Db has %d treadmill(s)%n", count);
} else {
System.out.println("Db is empty");
}
}
@Override
public void run(ApplicationArguments args) {
// here's you can print anything to the console
}
}
}Kotlin
@SpringBootApplication
class Application {
companion object {
@JvmStatic
fun main(args: Array<String>) {
SpringApplication.run(Application::class.java, *args)
}
}
@Component
inner class Runner(private val repository: TreadmillRepository) : ApplicationRunner {
private fun doWeHaveSomethingInDb() {
val count: Long = repository.count()
if (count > 0) {
System.out.printf("Db has %d treadmill(s)%n", count)
} else {
println("Db is empty")
}
}
override fun run(args: ApplicationArguments?) {
// here's you can print anything to the console
}
}
}In the doWeHaveSomethingInDb() method, we use the count method to check if the database has any data. The method name explains itself. It returns the number of objects in the database.
To keep the code shorter, we'll only share the code inside the run() method in the following sections because that's the only thing that will change.
Create
To store an entity in the database, create a new object and pass it to the save method. You can check your database before and after calling the save method in the following way:
Java
@Override
public void run(ApplicationArguments args) {
System.out.println("Before save:");
doWeHaveSomethingInDb();
System.out.println("Saving...");
repository.save(new Treadmill("aaa", "Yamaguchi runway"));
System.out.println("After save:");
doWeHaveSomethingInDb();
}Kotlin
override fun run(args: ApplicationArguments?) {
println("Before save:")
doWeHaveSomethingInDb()
println("Saving...")
repository.save(Treadmill("aaa", "Yamaguchi runway"))
println("After save:")
doWeHaveSomethingInDb()
}Before save:
Db is empty
Saving...
After save:
Db has 1 treadmill(s)The output confirms that the entity didn't exist before we called the save method. But after we call the save method, something appears in the database. "What exactly is it?" you might ask. To answer this question, we need to retrieve the data from the database.
Read
The CrudRepository has five methods to read data from the database. We've covered the count method in the previous section. Now let's discuss two more methods: findById and findAll. The other two methods, existsById and findAllById, are similar to the findById method. The only difference is that the existsById method returns a boolean flag indicating whether the entity with the requested ID is present in the database. The findAllById method allows requesting multiple entities by their ID at once.
Let's add some more data to our database: Treadmill("bbb", "Yamaguchi runway pro-x"), Treadmill("ccc", "Yamaguchi max"). You've already learned how to do so in the previous section.
The findById method returns an Optional object, which requires additional actions to access a real object. But if there is a probability of getting a null object, you need extra steps in any case, whether with an Optional or a manual null check.
As usual, we can print the details to see how it works:
Java
@Override
public void run(ApplicationArguments args) {
repository.save(new Treadmill("aaa", "Yamaguchi runway"));
repository.save(new Treadmill("bbb", "Yamaguchi runway pro-x"));
repository.save(new Treadmill("ccc", "Yamaguchi max"));
System.out.println("Looking for the treadmill with code='bbb'... ");
try {
Treadmill treadmill = repository.findById("bbb").orElseThrow(NoSuchElementException::new);
System.out.println(treadmill);
} catch (NoSuchElementException e) {
System.out.println("Not found");
}
}Kotlin
override fun run(args: ApplicationArguments?) {
repository.save(Treadmill("aaa", "Yamaguchi runway"))
repository.save(Treadmill("bbb", "Yamaguchi runway pro-x"))
repository.save(Treadmill("ccc", "Yamaguchi max"))
println("Looking for the treadmill with code='bbb'... ")
try {
val treadmill = repository.findById("bbb").orElseThrow { NoSuchElementException() }
println(treadmill)
} catch (e: NoSuchElementException) {
println("Not found")
}
}Looking for the treadmill with code='bbb'...
Treadmill(code: bbb, model: Yamaguchi runway pro-x)No surprises here. We knew that there was such an object because we'd just created it.
What if there is no object with the requested ID? Here is how it would go:
Java
@Override
public void run(ApplicationArguments args) {
repository.save(new Treadmill("aaa", "Yamaguchi runway"));
repository.save(new Treadmill("bbb", "Yamaguchi runway pro-x"));
repository.save(new Treadmill("ccc", "Yamaguchi max"));
System.out.println("Looking for the treadmill with code='non-existing-code'... ");
try {
Treadmill treadmill = repository.findById("non-existing-code").orElseThrow(NoSuchElementException::new);
System.out.println(treadmill);
} catch (NoSuchElementException e) {
System.out.println("Not found");
}
}Kotlin
override fun run(args: ApplicationArguments?) {
repository.save(Treadmill("aaa", "Yamaguchi runway"))
repository.save(Treadmill("bbb", "Yamaguchi runway pro-x"))
repository.save(Treadmill("ccc", "Yamaguchi max"))
println("Looking for the treadmill with code='non-existing-code'... ")
try {
val treadmill = repository.findById("non-existing-code").orElseThrow { NoSuchElementException() }
println(treadmill)
} catch (e: NoSuchElementException) {
println("Not found")
}
}As expected, we get the following:
Looking for the treadmill with code='non-existing-code'...
Not foundIf you want to get all entities from the database, the CrudRepository interface provides you with the findAll method. It's even simpler than findById:
Java
@Override
public void run(ApplicationArguments args) {
repository.save(new Treadmill("aaa", "Yamaguchi runway"));
repository.save(new Treadmill("bbb", "Yamaguchi runway pro-x"));
repository.save(new Treadmill("ccc", "Yamaguchi max"));
Iterable<Treadmill> treadmills = repository.findAll();
for (Treadmill treadmill : treadmills) {
System.out.println(treadmill);
}
}Kotlin
override fun run(args: ApplicationArguments?) {
repository.save(Treadmill("aaa", "Yamaguchi runway"))
repository.save(Treadmill("bbb", "Yamaguchi runway pro-x"))
repository.save(Treadmill("ccc", "Yamaguchi max"))
val treadmills = repository.findAll()
for (treadmill in treadmills) {
println(treadmill)
}
}Treadmill(code: aaa, model: Yamaguchi runway)
Treadmill(code: bbb, model: Yamaguchi runway pro-x)
Treadmill(code: ccc, model: Yamaguchi max)The output shows all our entities. If we don't have any entities in the database, the findAll method returns an empty Iterable object, and nothing will be printed. You can find it out for yourself.
If the database has many entities, the findAll method can lead to performance degradation or even an out-of-memory error. Use this method wisely.
Conclusion
Modern applications often include interactions with a database. CRUD is the acronym for the operations you can perform with data stored in a database: create, read, update, and delete. The Spring Data CrudRepository interface allows us to perform all these operations. Without it, we would have to implement these operations for each entity. Another important aspect is that CrudRepository is database-agnostic.
In this topic, we've covered the predefined operations from CrudRepository and looked at examples about create and read. Soon, you will learn about update and delete and also how to create your operations for your repository. Stay tuned!