How to avoid writing CRUD operations in every service: Spring Boot

How to avoid writing CRUD operations in every service: Spring Boot

As an API developer, we write the CRUD API majorly, but writing CRUD operations is easy and straightforward which practically takes seconds to copy-paste and refactoring the copied across services, But the project codebase has unwanted boilerplate code in logic part and  Unit testing code as long as we are writing simple CRUD operations we don't need to repeat these instead reuse the code we have written once.
So in this Spring Boot tutorial will be writing simple CRUD Service and use OOP practice to re-use our CRUD logic.

So I will be using be  Generics, Spring Boot Mongo Starter, and Web Starter packages, this can be replaced with traditional JPA Starter also.so now lets dive into the coding part, So for reference purpose, I have pasted my pom file

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ashrithgn</groupId>
    <artifactId>example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Reuse Crud example</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>junit</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
      
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.19</version>
                <dependencies>
                    <dependency>
                        <groupId>org.junit.platform</groupId>
                        <artifactId>junit-platform-surefire-provider</artifactId>
                        <version>${junit.platform.version}</version>
                    </dependency>
                    <dependency>
                        <groupId>org.junit.jupiter</groupId>
                        <artifactId>junit-jupiter-engine</artifactId>
                        <version>${junit.jupiter.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.5</version>
                <executions>
                    <execution>
                        <id>pre-unit-test</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>post-unit-test</id>
                        <phase>verify</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>

</project>
  1. Now we Shall create Base POJO contains routine audit fields used in the project.
public abstract class BaseModel implements Serializable {
    @Id
    private String id;
    private Date createdAt;
    private Date updateAt;
    private String createdBy;
    private String updatedBy;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }

    public Date getUpdateAt() {
        return updateAt;
    }

    public void setUpdateAt(Date updateAt) {
        this.updateAt = updateAt;
    }

    public String getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(String createdBy) {
        this.createdBy = createdBy;
    }

    public String getUpdatedBy() {
        return updatedBy;
    }

    public void setUpdatedBy(String updatedBy) {
        this.updatedBy = updatedBy;
    }
}

2 . Next, we shall construct Abstract Class which has common CRUD logic later can be extended for all the services

public abstract class BaseService<P extends BaseModel,R extends MongoRepository<P,String>> {
    @Autowired
    protected MongoTemplate template;

    @Autowired
    protected R repository;

    public P create(P input,String userId) {
        input.setId(null);
        input.setCreatedAt(new Date());
        input.setCreatedBy(userId);
        input.setUpdateAt(null);
        input.setUpdatedBy(null);
        repository.save(input);
        return input;
    }

    public boolean delete(String id) {
       P data = getById(id);
       repository.delete(data);
       return true;
    }

    public P update(P input,String id,String userId) {
        P data =  repository.findById(id).get();
        input.setId(data.getId());
        input.setCreatedAt(data.getCreatedAt());
        input.setCreatedBy(data.getCreatedBy());
        input.setUpdateAt(new Date());
        input.setUpdatedBy(userId);
        repository.save(input);
        return input;
    }

    public P getById(String id) {
       P data =  repository.findById(id).get();
       return data;
    }

    public P getBy(String key,String value,Class<P> className) {
        Query query = new Query();
        query.addCriteria(Criteria.where(key).is(value));
        query.limit(1);
        List<P> data = template.find(query,className);

        if(data.isEmpty() || data.size() == 0) {
            return null;
        }
        return data.get(0);
    }



}

If You're aware Generic of java we can pass place holder class while creating an instance, like how we use in Map implementation .BaseService<P extends BaseModel,R extends MongoRepository<P,String>> in this declaration, I have used POJO and its respective  Mongo repository, extends keywords enforce the base type we need to pass, As the above code is tightly coupled with MongoDB or other ORM we use, and fields certain common Audit fields.

3. On this step we can create a POJO like a profile for example

@Document(collection = "auth-users")
public class Profile extends BaseModel {
    private String name;
    @Indexed(unique = true)
    private String address;
    @JsonIgnore
    private String qualification;
    
    //TODO:ADD getter and setters
    
}

4. After creating POJO  lets create the  Mongo Repository for the same

public interface ProfileRepository extends MongoRepository<Profile,String> {

}

5. At last, this the service which we need to implement CRUD operation.

@Service
public class UserService extends BaseService<Users, UserRepository> {
  //Add Aditials Api service apart form crud
}

Final Step is Write controller and exposing the Service to an endpoint which can be consumed.

In case of writing service where you need to miniplate some fields like a password or need to disable the create and update method i.e like user service, you can refer to this following snippet.

@Service
public class UserService extends BaseService<Users, UserRepository> {

    PasswordComponent passwordComponent;
   


    public UserService(@Autowired PasswordComponent passwordComponent,
                       @Autowired MongoTemplate mongoTemplate,
                       @Autowired UserRepository userRepository,
    ) {
        this.passwordComponent = passwordComponent;
        super.repository = userRepository;
        super.template = mongoTemplate;

    }

    public Users create(String username, String password) {

        if (super.getBy("username", username, Users.class) != null) {
            throw new RuntimeException("user already exist");
        }

        Users users = new Users();
        users.setUsername(username);
        users.setPassword(passwordComponent.generatePassword(password));
        users.setActive(true);
        users.setVerified(true);
        //TODO:Send-Activation-link
        return super.create(users, "System");
    }

    @Override
    public Users create(Users input, String userId) {
        throw new RuntimeException("method is made private for security reason");
    }

    @Override
    public Users update(Users input, String id, String userId) {
        throw new RuntimeException("method is made private for security reason");
    }
}