19 minutes read

A typical application works with data stored in a database. Let's imagine such an application. What can it do with the data? First, this data must be stored (or created) in the database. Then, of course, we want the application to read the data. After reading, we might want to update or delete the data from the database. These operations are called CRUD operations, short for Create, Read, Update, and Delete.

In this topic, you will learn how to use CRUD operations to interact with a database from a Spring application. This topic is intentionally written as database-agnostic. It should work with any database supported by Spring Data.

Repositories in Spring

In Java and Kotlin, we manipulate database data using an entity. For example, we decide to open a fitness center and want to save all information about our fitness equipment. So far, we have a Treadmill. That's not a lot of equipment, right? But we've just started our business, so give us more time. We have to implement all CRUD operations for our entity. That's a little bit annoying, but it is just one entity. But then it turns out there will not be one but a few dozen different entities in the gym! Oops!

Fortunately, Spring data has the Repository concept. But what is it, and how can it help us? In Spring Data, a repository is an abstraction that helps us reduce the amount of boilerplate code. Spring Data provides several repository interfaces. All these interfaces are database-agnostic. This means you can use these abstractions with any relational or NoSQL database.

The base interface is Repository:

Java
@Indexed
public interface Repository<T, ID> {
}
Kotlin
@Indexed
interface Repository<T, ID> {
}

Did you notice the @Indexed annotation here? It indicates that all descendants of this interface should be treated as candidates for repository beans. Also, you can see that the Repository interface doesn't have any declared methods. That's because its purpose is to be a marker interface.

The Repository interface is generic. The generic type T represents an entity type, and the generic type ID represents the entity's unique ID type.

For CRUD operations, there is another interface, CrudRepository:

Java
@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();
}
Kotlin
@NoRepositoryBean
interface CrudRepository<T, ID> : Repository<T, ID> {
    // CREATE/UPDATE methods
    fun <S : T> save(entity: S): S
    fun <S : T> saveAll(entities: Iterable<S>): Iterable<S>

    // READ methods
    fun findById(id: ID): Optional<T>
    fun existsById(id: ID): Boolean
    fun findAll(): Iterable<T>
    fun findAllById(ids: Iterable<ID>): Iterable<T>
    fun count(): Long

    // DELETE methods
    fun deleteById(id: ID)
    fun delete(entity: T)
    fun deleteAllById(ids: Iterable<ID>)
    fun deleteAll(entities: Iterable<T>)
    fun deleteAll()
}

The CrudRepository interface contains operations for each CRUD action. As you can see, there are more than four methods: the interface allows passing an entity object and an entity ID to its methods. Additionally, you can work with a single entity or multiple entities at once.

You may have noticed that the CrudRepository interface has the same declared methods for the Create and Update operations. Under the hood, Spring data checks if a given entity is new or old and, depending on that, creates or updates the entity.

Don't worry if you can't remember all details of the above interfaces. The source code is always available and easy to find. The important part is to understand the main ideas behind CrudRepository.

Declaring a repository

Now we're fully equipped to create a repository.

Let's start with the Treadmill data class:

Java
// an annotation goes here
public class Treadmill {
    private String code;
    private String model;

    // constructors, getters, setters

}
Kotlin
// an annotation goes here
class Treadmill(
    var code: String,
    var model: String
)

You may wonder why we didn't annotate the Treadmill class. The reason is that the entity declaration can vary depending on the actual database you are using. Depending on your target database, you have to choose @Entity, @Document, @KeySpace, or another annotation.

To each entity its own... Wait, what is the thing an entity is mapped to? It depends on the database of your choice. It could be a table (for good old relational databases), a document (for modern NoSQL document-oriented databases like MongoDB), a node or an edge (the NoSQL graph database Neo4J is an example of that), or another type of user database.

To create our repository, we need to extend CrudRepository and specify the entity type (Treadmill) and entity ID type (String) as follows:

Java
public interface TreadmillRepository extends CrudRepository<Treadmill, String> {

}
Kotlin
interface TreadmillRepository : CrudRepository<Treadmill, String>

That's all. In this topic, we use the String property code as the ID, but you can use any type you want. In real-world applications, the Long type is commonly used, especially if you use a relational database and sequential numbers as an ID. Spring creates required implementations for all CRUD methods presented in the CrudRepository class.

Note that in our examples, we use a Treadmill entity and a TreadmillRepository. You will need to enrich the Treadmill entity with annotations for the database you're using.

Application Runner

Here is the template of the Application class used in our project:

Java
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

    @Component
    public class Runner implements CommandLineRunner {
        private final TreadmillRepository repository;

        public Runner(TreadmillRepository repository) {
            this.repository = repository;
        }

        @Override
        public void run(String... args) {
            // work with the repository here
        }
    }
}
Kotlin
@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

@Component
class Runner(private val repository: TreadmillRepository) : CommandLineRunner {
    
    override fun run(vararg args: String) {
        // work with the repository here
    }
}

You need to place all your work with the repository inside the run method.

Create

We use the count method to check if the database has any data. The method name is self-explanatory. It will return the number of objects we have in the database.

Java
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");
    }
}
Kotlin
private fun doWeHaveSomethingInDb() {
    val count = repository.count()
    if (count > 0) {
        println("Db has $count treadmill(s)")
    } else {
        println("Db is empty")
    }
}

To create an entity in the database, you must create a new object and pass it to the save method. We can check our database before and after calling the save method in the following way:

Java
System.out.println("Before save:");
doWeHaveSomethingInDb();
            
System.out.println("Saving...");
repository.save(new Treadmill("aaa", "Yamaguchi runway"));

System.out.println("After save:");
doWeHaveSomethingInDb();
Kotlin
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 proves that the entity didn't exist before calling the save method. After calling the save method, we can observe some data in the database. You may wonder what exactly that data represents. To answer this question, we read 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.

We will need a method to represent an entity as a string:

Java
private String createTreadmillView(Treadmill treadmill) {
    return "Treadmill(code: %s, model: %s)"
            .formatted(treadmill.getCode(), treadmill.getModel());
}
Kotlin
private fun createTreadmillView(treadmill: Treadmill): String {
    return "Treadmill(code: ${treadmill.code}, model: ${treadmill.model})"
}

In Java, you could also declare the toString() method inside the Treadmill entity. Note that the formatted method is a feature of Java 15. If you have a lower version of Java, you can use String.format.

In Kotlin, you could also declare the toString() method inside the Treadmill entity or mark the entity as a data class.

As usual, we can print the details to see how it works:

Java
System.out.println("Looking for the treadmill with code='bbb'... ");
Optional<Treadmill> treadmill = repository.findById("bbb");
String result = treadmill.map(this::createTreadmillView).orElse("Not found");
System.out.println(result);
Kotlin
println("Looking for the treadmill with code='bbb'... ")
val treadmill = repository.findById("bbb")
val result = treadmill.map { createTreadmillView(it) }.orElse("Not found")
println(result)
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
System.out.println("Looking for the treadmill with code='non-existing-code'... ");
Optional<Treadmill> treadmill = repository.findById("non-existing-code");
String result = treadmill.map(this::createTreadmillView).orElse("Not found");
System.out.println(result);
Kotlin
println("Looking for the treadmill with code='non-existing-code'... ")
val treadmill = repository.findById("non-existing-code")
val result = treadmill.map { createTreadmillView(it) }.orElse("Not found")
println(result)

As expected, we get the following:

Looking for the treadmill with code='non-existing-code'... 
Not found

If 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
Iterable<Treadmill> treadmills = repository.findAll();
for (Treadmill treadmill : treadmills) {
    System.out.println(createTreadmillView(treadmill));
}
Kotlin
val treadmills = repository.findAll()
for (treadmill in treadmills) {
    println(createTreadmillView(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.

Update

When we print all our treadmills to the output, we realize that the treadmill with the code aaa has the wrong model. It should be Yamaguchi runway-x instead of Yamaguchi runway. Here is how we can fix this:

Java
Optional<Treadmill> existingTreadmill = repository.findById("aaa");

String existing = existingTreadmill
        .map(this::createTreadmillView)
        .orElse("Not found");

System.out.println("Before update: " + existing);
System.out.println("Updating...");

existingTreadmill.ifPresent(treadmill -> {
    treadmill.setModel("Yamaguchi runway-x");
    repository.save(treadmill);
});

Optional<Treadmill> updatedTreadmill = repository.findById("aaa");
String updated = updatedTreadmill
        .map(this::createTreadmillView)
        .orElse("Not found");

System.out.println("After update: " + updated);
Kotlin
val existingTreadmill = repository.findById("aaa")

val existing = existingTreadmill
    .map { createTreadmillView(it) }
    .orElse("Not found")

println("Before update: $existing")
println("Updating...")

existingTreadmill.ifPresent {
    it.model = "Yamaguchi runway-x"
    repository.save(it)
}

val updatedTreadmill = repository.findById("aaa")
val updated: String = updatedTreadmill
    .map { createTreadmillView(it) }
    .orElse("Not found")

println("After update: $updated")

Just a reminder: save methods act as update methods when the database has an entity with the specified ID.

Before update: Treadmill(code: aaa, model: Yamaguchi runway)
Updating...
After update: Treadmill(code: aaa, model: Yamaguchi runway-x)

The output shows that everything works as expected, and our treadmill model has been updated.

Delete

The CrudRepository interface provides five methods for the delete action. We will cover the deleteById and delete methods. The deleteAllById and deleteAll methods work similarly but for a set of entities. The deleteAll method cleans up all your entities.

To show how it works, we introduce a new method:

Java
private void printAllTreadmills() {
    Iterable<Treadmill> treadmills = repository.findAll();
    for (Treadmill treadmill : treadmills) {
        System.out.println(createTreadmillView(treadmill));
    }
}
Kotlin
private fun printAllTreadmills() {
    val treadmills = repository.findAll()
    for (treadmill in treadmills) {
        println(createTreadmillView(treadmill))
    }
}

Three types of treadmills are a lot. We decided to delete the Yamaguchi max treadmill from our list. It has the code ccc:

Java
System.out.println("Before delete: ");
printAllTreadmills();

System.out.println("Deleting...");
repository.deleteById("ccc");

System.out.println("After delete: ");
printAllTreadmills();
Kotlin
println("Before delete: ")
printAllTreadmills()

println("Deleting...")
repository.deleteById("ccc")

println("After delete: ")
printAllTreadmills()
Before delete: 
Treadmill(code: aaa, model: yamaguchi runway-x)
Treadmill(code: bbb, model: Yamaguchi runway pro-x)
Treadmill(code: ccc, model: Yamaguchi max)
Deleting...
After delete: 
Treadmill(code: aaa, model: yamaguchi runway-x)
Treadmill(code: bbb, model: Yamaguchi runway pro-x)

Now there are only two treadmills in the database.

A fitness center is not a new idea. We've got a better one: we will open a co-working center equipped with height-adjustable desks and compact treadmills of the type Yamaguchi runway-x. So we need to delete Yamaguchi runway pro-x as well:

Java
System.out.println("Before delete: ");
printAllTreadmills();

System.out.println("Deleting...");
Optional<Treadmill> proXTreadmill = repository.findById("bbb");
proXTreadmill.ifPresent(
        treadmill -> {
            repository.delete(treadmill);
        }
);

System.out.println("After delete: ");
printAllTreadmills();
Kotlin
println("Before delete: ")
printAllTreadmills()

println("Deleting...")
val proXTreadmill = repository.findById("bbb")
proXTreadmill.ifPresent { repository.delete(it) }

println("After delete: ")
printAllTreadmills()
Before delete: 
Treadmill(code: aaa, model: yamaguchi runway-x)
Treadmill(code: bbb, model: Yamaguchi runway pro-x)
Deleting...
After delete: 
Treadmill(code: aaa, model: yamaguchi runway-x)

We have only one treadmill left and can start a new co-working center business.

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. Soon, you will learn how to create your operations for your repository. Stay tuned!

112 learners liked this piece of theory. 24 didn't like it. What about you?
Report a typo