Open all questions about Spring
In this article:
➤ What is ACID (Atomicity, Consistency, Isolation, Durability)
➤ What is CRUD (Create, Read, Update, Delete)
➤ What is JPA (Java Persistence API) and Hibernate
➤ What is Spring Data JPA
➤ What is JDBC (Java Database Connectivity)
➤ How JPA (Java Persistence API) differs from JDBC (Java Database Connectivity)
➤ What is JPQL and Criteria API
➤ How to Connect to Databases in Spring
➤ What are the transaction isolation levels?
➤ What are the parameters of the @Transactional annotation?
➤ How to use Redis in Spring
➤ How to handle database changes in Spring
➤ What are the repositories in Spring
➤ What is Jenkins
➤ What is Nexus
➤ What is a Query Plan?
➤ How to Use Indexes in Spring Database
➤ What anomalies can occur when working with a database
➤ What are the types of locking in the database
➤ What is ACID (Atomicity, Consistency, Isolation, Durability)
➤ What is CRUD (Create, Read, Update, Delete)
➤ What is JPA (Java Persistence API) and Hibernate
➤ What is Spring Data JPA
➤ What is JDBC (Java Database Connectivity)
➤ How JPA (Java Persistence API) differs from JDBC (Java Database Connectivity)
➤ What is JPQL and Criteria API
➤ How to Connect to Databases in Spring
➤ What are the transaction isolation levels?
➤ What are the parameters of the @Transactional annotation?
➤ How to use Redis in Spring
➤ How to handle database changes in Spring
➤ What are the repositories in Spring
➤ What is Jenkins
➤ What is Nexus
➤ What is a Query Plan?
➤ How to Use Indexes in Spring Database
➤ What anomalies can occur when working with a database
➤ What are the types of locking in the database
➤ What are the ways to manage transactions in Spring
➤ What is R2DBC (Reactive Relational Database Connectivity)
➤ What is Flyway, migrations
➤ Where should private data be stored in a Spring application?
➤ How to use caching in Spring
➤ How to use AWS / Amazon Web Services in Spring
➤ How to set up GitHub Actions Continuous Integration
➤ What is ACID (Atomicity, Consistency, Isolation, Durability)
A set of properties that ensure the reliability and predictability of transactions in database management systems (DBMS). The abbreviation stands for:
Atomicity:
The transaction is executed in its entirety or not executed at all.
If an error occurs at any stage, the system rolls back all changes made within that transaction.
Example:
If you transfer money from one account to another, the debit and credit operations must occur together. If the crediting fails, the debit is also cancelled.
Consistency:
After the transaction completes, the database must remain in a correct and consistent state.
This means that all constraints are respected (uniqueness, foreign keys, rules, etc.).
Example:
If the amount in the account cannot be negative, then no transaction should result in a violation of this rule.
Isolation:
Concurrent transactions must not interfere with each other.
Each transaction must be executed as if it were the only one in the system.
The DBMS uses isolation levels (Read Uncommitted, Read Committed, Repeatable Read, Serializable) to manage this property.
Example:
two users should not see each other's intermediate results.
Durability (Reliability / Constancy):
Once a transaction is successfully completed, its changes will not be lost even if a failure occurs (such as a power outage).
The DBMS writes changes to a log or to disk to ensure recovery.
Example:
If the transfer operation is completed, the money will definitely "go" and "come" – even if the server crashes.
Together, these properties enable reliable data management, especially under high load or failure conditions. They are the basis of transactional logic in most relational databases (PostgreSQL, MySQL, Oracle, etc.).
➤ What is CRUD (Create, Read, Update, Delete)
CRUD is an acronym that stands for the basic operations that can be performed on data in a database. CRUD stands for Create, Read, Update, Delete:
Create:
An operation to add new data to a database. In the context of Spring Data, this could be the save method, which saves a new entity to the database.
Read:
An operation to retrieve data from a database. In Spring Data, these are methods like findById, findAll, and custom methods for searching by various criteria, such as findByName.
Update:
An operation that modifies existing data in a database. In Spring Data, this is also done using the save method, which updates the entity if it already exists.
Delete:
The operation of deleting data from the database. In Spring Data, these are the deleteById and delete methods.
Spring Data has several standard methods for working with repositories. These methods can be used for basic CRUD (Create, Read, Update, Delete) operations. Examples of using repositories with Kotlin and Gradle Groovy are shown below.
The main standard methods in Spring Data repositories are:
save(S entity: T): T:
saves or updates an entity.
findById(id: ID): Optional:
searches for an entity by its ID.
findAll(): List:
returns all entities.
deleteById(id: ID):
deletes an entity by its ID.
delete(entity: T):
Deletes the specified entity.
existsById(id: ID): Boolean:
checks the existence of an entity by its ID.
count(): Long:
returns the number of entities.
An example of implementation, using standard methods and one custom one:
import javax.persistence.Entity import javax.persistence.GeneratedValue import javax.persistence.GenerationType import javax.persistence.Id @Entity data class Product( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, val name: String, val price: Double )
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository interface ProductRepository : JpaRepository<Product, Long> { fun findByName(name: String): List<Product> }
import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/products") class ProductController(@Autowired val productRepository: ProductRepository) { // Create @PostMapping fun createProduct(@RequestBody product: Product): Product { return productRepository.save(product) } // Read all @GetMapping fun getAllProducts(): List<Product> { return productRepository.findAll() } // Read by ID @GetMapping("/{id}") fun getProductById(@PathVariable id: Long): Product? { return productRepository.findById(id).orElse(null) } // Read by name @GetMapping("/search") fun getProductsByName(@RequestParam name: String): List<Product> { return productRepository.findByName(name) } // Update @PutMapping("/{id}") fun updateProduct(@PathVariable id: Long, @RequestBody updatedProduct: Product): Product? { return productRepository.findById(id).map { existingProduct -> val newProduct = existingProduct.copy(name = updatedProduct.name, price = updatedProduct.price) productRepository.save(newProduct) }.orElse(null) } // Delete by ID @DeleteMapping("/{id}") fun deleteProduct(@PathVariable id: Long) { productRepository.deleteById(id) } // Delete @DeleteMapping fun deleteProduct(@RequestBody product: Product) { productRepository.delete(product) } // Check existence by ID @GetMapping("/exists/{id}") fun existsById(@PathVariable id: Long): Boolean { return productRepository.existsById(id) } // Count all products @GetMapping("/count") fun countProducts(): Long { return productRepository.count() } }
➤ What is JPA (Java Persistence API) and Hibernate
Hibernate and JPA (Java Persistence API) are two closely related terms in the world of Java projects related to object-relational mapping (ORM). It is important to understand their differences and relationships.
JPA (Java Persistence API):
JPA is a standard specification designed to provide ORM in Java applications. It defines interfaces and annotations for managing persistent data and its relationships. JPA is not a specific implementation; it is simply a set of rules and standards that specific ORM frameworks must implement.
Key features of JPA:
Specification for ORM.
Does not contain implementation.
Defines annotations and interfaces such as @Entity, @Table, EntityManager, etc.
Provides independence from the specific ORM provider.
Hibernate:
Hibernate is an ORM framework that is one of the implementations of the JPA specification. It provides specific tools and mechanisms for working with JPA-based databases, and also includes additional capabilities beyond JPA.
Key features of Hibernate:
A concrete ORM implementation that conforms to JPA.
Additional features such as caching, advanced association types, more flexible query criteria, etc.
Custom annotations and configurations (e.g. @Cache, @BatchSize).
Extensibility and many additional tools to optimize performance.
Interaction between JPA and Hibernate
When you use JPA, you write code that follows the JPA specification. This code will work with any JPA implementation, including Hibernate, EclipseLink, and others. Hibernate, in turn, provides specific tools for executing JPA code, and also adds its own capabilities.
Example of using JPA with Hibernate:
// Hibernate-specific configuration (hibernate.cfg.xml) <hibernate-configuration> <session-factory> <!-- Database connection settings --> <property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property> <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/mydb</property> <property name="hibernate.connection.username">root</property> <property name="hibernate.connection.password">password</property> <!-- JDBC connection pool settings ... using built-in test pool --> <property name="hibernate.c3p0.min_size">5</property> <property name="hibernate.c3p0.max_size">20</property> <property name="hibernate.c3p0.timeout">300</property> <property name="hibernate.c3p0.max_statements">50</property> <property name="hibernate.c3p0.idle_test_period">3000</property> <!-- Echo all executed SQL to stdout --> <property name="hibernate.show_sql">true</property> <!-- Drop and re-create the database schema on startup --> <property name="hibernate.hbm2ddl.auto">update</property> <!-- Names the annotated entity class --> <mapping class="com.example.User"/> </session-factory> </hibernate-configuration>
// JPA Entity import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; } // JPA Repository import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.transaction.Transactional; public class UserRepository { @PersistenceContext private EntityManager entityManager; @Transactional public void save(User user) { entityManager.persist(user); } public User find(Long id) { return entityManager.find(User.class, id); } }
So, JPA is a specification for ORM, and Hibernate is a framework that implements this specification and adds its own capabilities.
➤ What is Spring Data JPA
Spring Data JPA is a part of the Spring Data project designed to simplify working with JPA (Java Persistence API)-based databases. It is a high-level library that provides an abstraction over JPA and simplifies development by reducing the amount of boilerplate code required to perform basic database operations such as create, read, update, and delete (CRUD). Spring Data JPA offers many useful features such as automatic query generation, support for name-based query methods, and integration with various storage engines.
The main advantages of Spring Data JPA:
Simplifying work with the database:
Reduces the amount of boilerplate code required to perform database operations.
Support for standard CRUD operations:
Provides built-in methods for performing basic data operations.
Automatic generation of queries:
Allows you to automatically generate queries based on repository method names.
Support for complex queries:
Supports writing JPQL, Native SQL and Criteria API for more complex queries.
Extensibility:
Easily extendable and customizable to suit application needs.
The main components of Spring Data JPA are:
Repository:
Interfaces that provide methods for performing database operations.
Entity:
Classes representing database entities.
JPQL and Native Queries:
Ability to use Java Persistence Query Language and native SQL queries to perform database operations.
Custom Queries:
Ability to create custom methods to perform specific database operations.
➤ What is JDBC (Java Database Connectivity)
Java Database Connectivity (JDBC) is an API for the Java programming language that defines how a client can access a database. JDBC is part of the Java Standard Edition and provides developers with a standard interface for interacting with a variety of relational databases.
Key features of JDBC:
Low-level API:
JDBC provides low-level access to databases, allowing developers to execute SQL queries directly and retrieve results.
SQL queries:
JDBC allows you to execute SQL queries (SELECT, INSERT, UPDATE, DELETE) and other database operations.
Connection Management:
JDBC manages connections to the database, which includes opening and closing connections, handling transactions, and managing connection pools.
Compatibility with various DBMS:
JDBC supports working with various relational databases such as MySQL, PostgreSQL, Oracle, SQL Server and others.
JDBC Core Components
JDBC Driver:
A JDBC driver is a database-specific implementation of the JDBC interfaces. It provides communication between a Java application and a database.
There are four types of JDBC drivers:
type 1 (JDBC-ODBC Bridge)
type 2 (Native API)
type 3 (Network Protocol)
type 4 (Thin Driver, Pure Java Driver).
Connection:
The Connection interface represents a connection to a database. It is used to send queries and manage transactions.
Statement:
The Statement interface is used to execute static SQL queries and return results.
There are three types of Statement: Statement, PreparedStatement (for executing precompiled SQL queries), and CallableStatement (for calling stored procedures).
ResultSet:
The ResultSet interface represents the result set of data returned by a SQL query. It allows you to navigate through the results and retrieve data.
SQLException:
The SQLException class is used to handle errors and exceptions that occur when working with JDBC.
Example of using JDBC:
import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class JdbcExample { public static void main(String[] args) { String url = "jdbc:mysql://localhost:3306/mydatabase"; String username = "root"; String password = "password"; try (Connection connection = DriverManager.getConnection(url, username, password); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT id, name FROM users")) { while (resultSet.next()) { int id = resultSet.getInt("id"); String name = resultSet.getString("name"); System.out.println("User ID: " + id + ", Name: " + name); } } catch (SQLException e) { e.printStackTrace(); } } }
➤ How JPA (Java Persistence API) differs from JDBC (Java Database Connectivity)
Java Persistence API (JPA) and Java Database Connectivity (JDBC) are two different approaches to working with databases in Java. Here are the main differences between them:
JDBC (Java Database Connectivity):
Abstraction level:
JDBC provides a low-level API for interacting with databases.
Working via JDBC requires writing SQL queries manually and processing query results via ResultSet.
Direct access to the database:
JDBC allows the developer to work directly with the database using SQL queries.
The developer must manage connections, transactions, and error handling himself.
Less abstraction:
JDBC provides a lower level of abstraction, making the code more verbose and difficult to maintain.
Requires writing a lot of code to perform simple database operations.
Flexibility:
JDBC allows you to use all the capabilities of a specific DBMS, including its specific functions and extensions.
JPA (Java Persistence API):
Abstraction level:
JPA provides a higher-level API for working with object-relational mapping (ORM).
Developers work with Java objects instead of writing SQL queries directly.
Object-relational mapping:
JPA automatically maps Java objects to database tables.
This allows you to work with the database at the object level, which simplifies code development and maintenance.
State Management:
JPA supports object state management, automatic change tracking, and database synchronization.
JPA automatically handles transactions, caching, and other tasks.
Less code:
JPA significantly reduces the amount of code required to perform database operations.
Using annotations and configuration files makes it easy to customize and modify the data model.
Cross-platform:
JPA provides an abstraction that allows you to work with different DBMSs without changing your code.
Example for comparison:
JDBC:
public List<User> getUsers() { List<User> users = new ArrayList<>(); try (Connection connection = DriverManager.getConnection(url, username, password); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT id, name FROM users")) { while (resultSet.next()) { User user = new User(); user.setId(resultSet.getInt("id")); user.setName(resultSet.getString("name")); users.add(user); } } catch (SQLException e) { e.printStackTrace(); } return users; }
JPA:
public List<User> getUsers() { EntityManager em = entityManagerFactory.createEntityManager(); return em.createQuery("SELECT u FROM User u", User.class).getResultList(); }
Using JPA and JDBC together:
can be useful in cases where you need to execute complex SQL queries that are not supported by JPA, or to optimize performance when working with large amounts of data. Here is an example demonstrating how you can use JPA to manage entities and JDBC to execute specific queries.
dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'com.h2database:h2' // H2 for testing }
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // getters and setters }
import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository<User, Long> { }
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; import javax.transaction.Transactional; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; @Service public class UserService { @Autowired private UserRepository userRepository; @Autowired private JdbcTemplate jdbcTemplate; @Transactional public void createUser(String name) { User user = new User(); user.setName(name); userRepository.save(user); } public List<User> getAllUsersJPA() { return userRepository.findAll(); } public List<User> getAllUsersJDBC() { String sql = "SELECT id, name FROM User"; return jdbcTemplate.query(sql, new UserRowMapper()); } private static class UserRowMapper implements RowMapper<User> { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setId(rs.getLong("id")); user.setName(rs.getString("name")); return user; } } }
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class UserController { @Autowired private UserService userService; @PostMapping("/users") public void createUser(@RequestParam String name) { userService.createUser(name); } @GetMapping("/users/jpa") public List<User> getUsersJPA() { return userService.getAllUsersJPA(); } @GetMapping("/users/jdbc") public List<User> getUsersJDBC() { return userService.getAllUsersJDBC(); } }
JPA Entity and Repository:
The User class represents the database entity, and the interface
UserRepository is used for standard CRUD operations using JPA.
Service layer:
The UserService class contains the business logic. The createUser method uses JPA to create a new user. The getAllUsersJPA and getAllUsersJDBC methods demonstrate using JPA and JDBC, respectively, to retrieve a list of users.
Controller:
The UserController class provides a REST API for interacting with the service. The getUsersJPA and getUsersJDBC methods call the corresponding service methods to retrieve data using JPA and JDBC.
This way, you can take advantage of the benefits of both technologies: the convenience and abstraction of JPA for standard operations and the flexibility of JDBC for complex queries.
➤ What is JPQL and Criteria API
PQL and Criteria API in Spring Data JPA:
JPQL (Java Persistence Query Language) and Criteria API provide flexible ways to query the database using JPA. Both approaches are supported by Spring Data JPA and can be used depending on the application requirements.
JPQL (Java Persistence Query Language):
JPQL is an object-oriented query language that operates on JPA objects rather than database tables. JPQL queries resemble SQL, but operate on entities, their attributes, and relationships.
Example of using JPQL:
Let's look at an example where we'll use JPQL to perform queries. In this example, the findByNameJPQL method uses the @Query annotation to perform a JPQL query that finds users by name.
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository @Repository interface UserRepository : JpaRepository<User, Long> { @Query("SELECT u FROM User u WHERE u.name = :name") fun findByNameJPQL(name: String): List<User> }
Criteria API:
The Criteria API provides a type-safe and object-oriented way to create database queries. It is especially useful when queries need to be built dynamically.
Example of using Criteria API:
To use the Criteria API we need to use the EntityManager which manages the JPA operations.
We create an interface for defining custom methods.
interface UserRepositoryCustom { fun findByNameCriteria(name: String): List<User> }
We implement the interface using the Criteria API.
import org.springframework.stereotype.Repository import javax.persistence.EntityManager import javax.persistence.PersistenceContext import javax.persistence.criteria.CriteriaBuilder import javax.persistence.criteria.CriteriaQuery import javax.persistence.criteria.Root @Repository class UserRepositoryCustomImpl : UserRepositoryCustom { @PersistenceContext private lateinit var entityManager: EntityManager override fun findByNameCriteria(name: String): List<User> { val criteriaBuilder: CriteriaBuilder = entityManager.criteriaBuilder val criteriaQuery: CriteriaQuery<User> = criteriaBuilder.createQuery(User::class.java) val root: Root<User> = criteriaQuery.from(User::class.java) criteriaQuery.select(root).where(criteriaBuilder.equal(root.get<String>("name"), name)) val query = entityManager.createQuery(criteriaQuery) return query.resultList } }
Extending the main repository with a custom interface.
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository interface UserRepository : JpaRepository<User, Long>, UserRepositoryCustom
➤ How to Connect to Databases in Spring
In Spring Boot, you can easily configure connections to different types of databases using application.properties configuration files. Below are examples of configuring connections to various popular databases: H2, MySQL, PostgreSQL, Oracle, MariaDB.
Connecting to H2 (in memory):
H2 is an embedded database that is often used for development and testing.
dependencies { implementation("com.h2database:h2") }
# H2 Database Configuration spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true spring.jpa.hibernate.ddl-auto=update
Connecting to MySQL:
dependencies { implementation("mysql:mysql-connector-java") }
# MySQL Database Configuration spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver spring.datasource.username=myuser spring.datasource.password=mypassword spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect spring.jpa.hibernate.ddl-auto=update
Connecting to PostgreSQL:
dependencies { implementation("org.postgresql:postgresql") }
# PostgreSQL Database Configuration spring.datasource.url=jdbc:postgresql://localhost:5432/mydatabase spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.username=myuser spring.datasource.password=mypassword spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update
Connecting to Oracle:
dependencies { implementation("com.oracle.database.jdbc:ojdbc8") }
# Oracle Database Configuration spring.datasource.url=jdbc:oracle:thin:@localhost:1521:orcl spring.datasource.driverClassName=oracle.jdbc.OracleDriver spring.datasource.username=myuser spring.datasource.password=mypassword spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect spring.jpa.hibernate.ddl-auto=update
Connecting to MongoDB (NoSQL):
dependencies { implementation("org.springframework.boot:spring-boot-starter-data-mongodb") }
spring.data.mongodb.host=localhost spring.data.mongodb.port=27017 spring.data.mongodb.database=mydatabase
import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document @Document(collection = "users") data class User( @Id val id: String? = null, val name: String, val email: String )
import org.springframework.data.mongodb.repository.MongoRepository import org.springframework.stereotype.Repository @Repository interface UserRepository : MongoRepository<User, String> { fun findByName(name: String): User }
Connecting to MariaDB:
dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.mariadb.jdbc:mariadb-java-client' runtimeOnly 'org.springframework.boot:spring-boot-devtools' testImplementation 'org.springframework.boot:spring-boot-starter-test' }
spring.datasource.url=jdbc:mariadb://localhost:3306/mydatabase spring.datasource.username=root spring.datasource.password=yourpassword spring.datasource.driver-class-name=org.mariadb.jdbc.Driver spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
Connecting to Couchbase (NoSQL):
dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-couchbase' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' implementation 'org.jetbrains.kotlin:kotlin-reflect' implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' testImplementation 'org.springframework.boot:spring-boot-starter-test' }
spring.couchbase.connection-string=127.0.0.1 spring.couchbase.username=your_username spring.couchbase.password=your_password spring.couchbase.bucket.name=your_bucket
import org.springframework.context.annotation.Configuration import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration @Configuration class CouchbaseConfig : AbstractCouchbaseConfiguration() { override fun getConnectionString(): String { return "127.0.0.1" } override fun getUserName(): String { return "your_username" } override fun getPassword(): String { return "your_password" } override fun getBucketName(): String { return "your_bucket" } }
import com.example.model.User import org.springframework.data.couchbase.repository.CouchbaseRepository import org.springframework.stereotype.Repository @Repository interface UserRepository : CouchbaseRepository<User, String> { fun findByEmail(email: String): List<User> }
Setting up a connection pool:
Regardless of the database you use, you can configure connection pooling to improve performance.
# Connection Pool Configuration spring.datasource.hikari.maximum-pool-size=10 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.idle-timeout=30000 spring.datasource.hikari.max-lifetime=1800000 spring.datasource.hikari.connection-timeout=30000
➤ What are the transaction isolation levels?
Transaction isolation levels determine the degree to which data changes made by one transaction are visible to other concurrent transactions. This is important for managing concurrent access to data and preventing various anomalies when transactions are executed simultaneously. The SQL standard and most relational DBMSs define four transaction isolation levels:
Isolation Level: | Dirty Read | Non-Repeatable Read | Phantom Read --------------------------------------------------------------------------------- READ UNCOMMITTED | + | + | + READ COMMITTED | - | + | + REPEATABLE READ | - | - | + SERIALIZABLE | - | - | -
Read Uncommitted:
Description: This isolation level allows a transaction to read data that has been modified but not yet committed by other transactions.
Anomalies:
Dirty Read: Reading data that can be rolled back in another transaction.
Non-repeatable Read: Reading data that may be modified by another transaction between the two reads.
Phantom Read: Reading rows that might be added or deleted by another transaction.
Read Committed:
Description: A transaction can only read data that has been committed by other transactions. This prevents dirty reads.
Anomalies:
Non-repeatable Read: Reading data that may be modified by another transaction between the two reads.
Phantom Read: Reading rows that might be added or deleted by another transaction.
Repeatable Read:
Description: This isolation level ensures that data read at the beginning of a transaction is not modified by other transactions until the current transaction completes. This prevents both dirty reads and non-repeatable reads.
Anomalies:
Phantom Read: Reading rows that might be added or deleted by another transaction.
Serializable:
Description: The highest isolation level, where transactions are executed as if they were running sequentially, one after another, rather than in parallel. This prevents all kinds of anomalies.
Anomalies:
Not allowed. Ensures that there are no dirty reads, non-repeatable reads, or phantom reads.
Explanation of anomalies:
Dirty Read:
One transaction reads data modified by another transaction that has not yet committed. If the second transaction is rolled back, the first transaction uses incorrect data.
Non-repeatable Read:
Data read by one transaction may be modified by other transactions, causing repeated readings of the same data to return different results.
Phantom Read:
One transaction reads a set of rows that match a certain condition. If another transaction adds or deletes rows that match that condition, re-executing the query in the first transaction returns a different set of rows.
Examples of using isolation levels in Spring:
In Spring, transaction isolation level can be specified using the @Transactional annotation, there are other ways too
import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Isolation import org.springframework.transaction.annotation.Transactional @Service class MyService { @Transactional(isolation = Isolation.READ_UNCOMMITTED) fun performReadUncommittedOperation() { } }
➤ What are the parameters of the @Transactional annotation?
propagation (type: Propagation):
Specifies how the transaction should be propagated to methods called within the current transaction.
Values:
Propagation.REQUIRED (default):
Uses the current transaction or creates a new one if there is no current one.
Propagation.REQUIRES_NEW:
Always creates a new transaction, suspending the current one if it exists.
Propagation.SUPPORTS:
Uses the current transaction if one exists, otherwise executes without a transaction.
Propagation.NOT_SUPPORTED:
Executes a method outside of a transaction, suspending the current transaction if one exists.
Propagation.MANDATORY:
Requires the existence of a current transaction, throws an exception if one does not exist.
Propagation.NEVER:
Runs without a transaction, throws an exception if a current transaction exists.
Propagation.NESTED:
Creates a nested transaction if a current transaction exists.
@Transactional(propagation = Propagation.REQUIRED) fun myTransactionalMethod() {
isolation (type: Isolation):
Defines the transaction isolation level.
Values:
Isolation.DEFAULT (default):
Uses the database isolation level.
Isolation.READ_UNCOMMITTED:
The lowest isolation level, allows “dirty reads”.
Isolation.READ_COMMITTED:
Ensures that the data read in a transaction has already been committed.
Isolation.REPEATABLE_READ:
Ensures that the data read in a transaction is not changed until the transaction completes.
Isolation.SERIALIZABLE:
The highest level of isolation, guarantees complete transaction isolation.
@Transactional(isolation = Isolation.REPEATABLE_READ) fun myTransactionalMethod() { // The code will be executed within a transaction with the REPEATABLE_READ isolation level. }
timeout (type: Int):
Specifies the maximum time (in seconds) a transaction can execute before being forced to roll back.
The default value of -1 means no time limit.
If the transaction does not complete within the specified time, a TransactionTimedOutException will be thrown.
@Transactional(timeout = 30) fun myTransactionalMethod() { // The code must be executed within 30 seconds. }
readOnly (type: Boolean):
Specifies that the transaction is read-only. The default value is false.
Setting this to true will optimize performance since the transaction will not contain any changes.
@Transactional(readOnly = true) fun myReadOnlyMethod() { // The code is executed in read-only mode. }
rollbackFor (type: Array<Class<out Throwable>>):
Specifies an array of exceptions that, if encountered, should cause the transaction to be rolled back.
@Transactional(rollbackFor = [RuntimeException::class, IOException::class]) fun myMethod() { // The transaction will be rolled back if a RuntimeException or IOException is thrown. }
rollbackForClassName (type: Array<String>):
Specifies an array of exception names (as strings) that, when raised, should cause the transaction to roll back.
noRollbackFor (type: Array<Class<out Throwable>>):
Specifies an array of exceptions that should prevent the transaction from being rolled back.
@Transactional(noRollbackFor = [CustomException::class]) fun myMethod() { // The transaction will not be rolled back if a CustomException is thrown. }
noRollbackForClassName (type: Array<String>):
Specifies an array of exception names (as strings) that should not roll back the transaction.
transactionManager:
Specifies which transaction manager to use if there are multiple.
@Transactional(transactionManager = "customTransactionManager") fun myMethod() { // The method will use customTransactionManager }
value:
Synonym for transactionManager. Can be used to specify a transaction manager.
@Transactional("customTransactionManager") fun myMethod() { // The method will use customTransactionManager }
Example of using all parameters:
import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Isolation import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional @Service class MyService { @Transactional( propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ, timeout = 30, readOnly = false, rollbackFor = [RuntimeException::class], noRollbackFor = [CustomException::class], transactionManager = "customTransactionManager" ) fun myTransactionalMethod() { } }
Spring's @Transactional mechanism is based on the use of proxies and aspect-oriented programming to manage transactions. The proxy intercepts method calls and interacts with the transaction manager to start, commit, or roll back transactions. This allows for automatic transaction handling, making it easier to develop robust and consistent applications.
To monitor transactions in logs in Spring applications, you need to configure logging of the corresponding Spring components and Hibernate (if JPA/Hibernate is used). The main focus should be on transaction start, commit, rollback, and potential errors.
logging: level: org: springframework: transaction: DEBUG hibernate: SQL: DEBUG transaction: DEBUG type: descriptor: sql: BasicBinder: TRACE
Example logging configuration for Logback (logback-spring.xml):
<configuration> <include resource="org/springframework/boot/logging/logback/base.xml"/> <logger name="org.springframework.transaction" level="DEBUG"/> <logger name="org.hibernate.SQL" level="DEBUG"/> <logger name="org.hibernate.transaction" level="DEBUG"/> <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> </root> </configuration>
What to look for in logs:
Transaction start:
Messages indicating the start of a new transaction.
For example: “Creating new transaction with name […]”
Transaction commit:
Transaction commit messages.
For example: “Initiating transaction commit for […]”
Rollback transaction:
Messages about transaction rollback.
For example: “Initiating transaction rollback for […]”
Errors and exceptions:
Error and exception messages related to transactions.
For example: “Rolling back transaction because of exception […]”
Monitoring tools:
Spring Boot Actuator:
Allows you to monitor the state of your application, including transaction information.
Application Performance Monitoring (APM) Tools:
Tools like New Relic, Dynatrace, or AppDynamics provide detailed information about transactions, requests, performance, and errors.
➤ How to use Redis in Spring
Redis (Remote Dictionary Server) is a high-performance, in-memory key-value store that supports a variety of data structures, such as strings, lists, sets, hashes, and sorted sets. Redis is often used for data caching, session management, message queues, chats, counters, and other scenarios that require high performance and low latency.
Integrating Spring Boot with Redis can be done using Spring Data Redis, which provides a convenient and powerful API for working with Redis. Here is a step-by-step example of how to configure and use Redis in a Spring Boot application.
dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' }
spring.redis.host=localhost spring.redis.port=6379
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.data.redis.connection.RedisConnectionFactory import org.springframework.data.redis.core.RedisTemplate import org.springframework.data.redis.serializer.GenericToStringSerializer @Configuration class RedisConfig { @Bean fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<String, Any> { val template = RedisTemplate<String, Any>() template.setConnectionFactory(connectionFactory) template.keySerializer = GenericToStringSerializer(String::class.java) template.valueSerializer = GenericToStringSerializer(Any::class.java) return template } }
import java.io.Serializable data class Person( val id: String, val name: String, val age: Int ) : Serializable
import org.springframework.beans.factory.annotation.Autowired import org.springframework.data.redis.core.RedisTemplate import org.springframework.stereotype.Service @Service class PersonService(@Autowired private val redisTemplate: RedisTemplate<String, Any>) { private val hashKey = "Person" fun save(person: Person) { redisTemplate.opsForHash<String, Person>().put(hashKey, person.id, person) } fun findById(id: String): Person? { return redisTemplate.opsForHash<String, Person>().get(hashKey, id) } fun findAll(): List<Person> { return redisTemplate.opsForHash<String, Person>().values(hashKey) as List<Person> } fun delete(id: String) { redisTemplate.opsForHash<String, Person>().delete(hashKey, id) } }
import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/persons") class PersonController(@Autowired private val personService: PersonService) { @PostMapping fun savePerson(@RequestBody person: Person) { personService.save(person) } @GetMapping("/{id}") fun getPerson(@PathVariable id: String): Person? { return personService.findById(id) } @GetMapping fun getAllPersons(): List<Person> { return personService.findAll() } @DeleteMapping("/{id}") fun deletePerson(@PathVariable id: String) { personService.delete(id) } }
➤ How to handle database changes in Spring
In a Spring application, you can use approaches to monitor changes in the database and perform actions, such as sending a query, when a value in the database changes. One popular way to achieve this is to use triggers on the database along with Spring Events or, if using a NoSQL database such as MongoDB, using its Change Streams functionality.
Example for a relational database (eg PostgreSQL) using triggers and listeners:
Creating a trigger in the database:
Create a trigger in the database to track changes to the table.
Example for PostgreSQL:
Create a trigger function
CREATE OR REPLACE FUNCTION notify_table_update() RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify('table_update', NEW.id::text); RETURN NEW; END; $$ LANGUAGE plpgsql;
Create a trigger that calls this function when a row in the table is updated:
CREATE TRIGGER table_update_trigger AFTER UPDATE ON your_table FOR EACH ROW EXECUTE FUNCTION notify_table_update();
Setting up a listener in Spring Boot:
Use JDBC to listen for notifications from the database.
spring.datasource.url=jdbc:postgresql://localhost:5432/your_database spring.datasource.username=your_username spring.datasource.password=your_password spring.datasource.driver-class-name=org.postgresql.Driver
import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Service import javax.annotation.PostConstruct @Service class NotificationListener(private val jdbcTemplate: JdbcTemplate) { @PostConstruct fun listenForNotifications() { Thread { try { jdbcTemplate.dataSource?.connection?.use { connection -> connection.createStatement().use { statement -> statement.execute("LISTEN table_update") } while (true) { connection.unwrap(org.postgresql.PGConnection::class.java).getNotifications()?.let { notifications -> for (notification in notifications) { println("Received notification: ${notification.parameter}") // Submit a request when you receive a notification sendRequest(notification.parameter) } } Thread.sleep(1000) // Set a delay between checks } } } catch (e: Exception) { e.printStackTrace() } }.start() } private fun sendRequest(id: String) { // Logic for sending a request println("Sending request for id: $id") // For example, you can use RestTemplate or WebClient to send an HTTP request. } }
Example for MongoDB using Change Streams:
If you are using MongoDB, you can use Change Streams to track changes.
spring.data.mongodb.uri=mongodb://localhost:27017/your_database
import com.mongodb.client.MongoClients import com.mongodb.client.model.changestream.ChangeStreamDocument import org.bson.Document import org.springframework.stereotype.Service import javax.annotation.PostConstruct @Service class ChangeStreamListener { @PostConstruct fun listenForChanges() { Thread { try { MongoClients.create("mongodb://localhost:27017").use { mongoClient -> val database = mongoClient.getDatabase("your_database") val collection = database.getCollection("your_collection") val changeStream = collection.watch() for (change in changeStream) { println("Received change: ${change.fullDocument}") // Send a request when you receive a change sendRequest(change.fullDocument) } } } catch (e: Exception) { e.printStackTrace() } }.start() } private fun sendRequest(document: Document?) { // Request sending logic println("Sending request for document: $document") // For example, you can use RestTemplate or WebClient to send an HTTP request. } }
➤ What are the repositories in Spring
Repository ├── CrudRepository │ ├── PagingAndSortingRepository │ │ ├── JpaRepository │ │ └── MongoRepository │ └── ReactiveCrudRepository │ └── R2dbcRepository
CrudRepository:
Provides basic CRUD (create, read, update, delete) methods for working with the database. It is the base interface for all repositories in Spring Data.
import org.springframework.data.repository.CrudRepository interface PersonRepository : CrudRepository<Person, Long>
JpaRepository:
extends CrudRepository and PagingAndSortingRepository by providing additional methods for working with JPA (Java Persistence API), such as methods for batch operations and methods for working with flashing.
import org.springframework.data.jpa.repository.JpaRepository interface PersonRepository : JpaRepository<Person, Long>
PagingAndSortingRepository:
extends CrudRepository and provides methods for performing sorting and pagination operations.
import org.springframework.data.repository.PagingAndSortingRepository interface PersonRepository : PagingAndSortingRepository<Person, Long>
MongoRepository:
is used to work with MongoDB. It extends PagingAndSortingRepository and provides methods for working with MongoDB.
import org.springframework.data.mongodb.repository.MongoRepository interface PersonRepository : MongoRepository<Person, String>
ReactiveCrudRepository:
is used for reactive programming with support from Project Reactor. It provides asynchronous methods for CRUD operations.
import org.springframework.data.repository.reactive.ReactiveCrudRepository import reactor.core.publisher.Mono interface PersonRepository : ReactiveCrudRepository<Person, Long> { fun findByFirstName(firstName: String): Mono<Person> }
R2dbcRepository:
used to work with databases using R2DBC (Reactive Relational Database Connectivity).
import org.springframework.data.r2dbc.repository.R2dbcRepository import reactor.core.publisher.Mono interface PersonRepository : R2dbcRepository<Person, Long> { fun findByLastName(lastName: String): Mono<Person> }
➤ What is Jenkins
Jenkins is a popular Continuous Integration (CI) and Continuous Deployment (CD) tool. It is used to automate various stages of software development, such as building, testing, and deploying applications. Jenkins is an open-source project that supports many plugins for integration with various tools and services.
Key features of Jenkins:
Process automation:
Jenkins allows you to automate the building, testing, and deployment of applications. This helps reduce manual work and reduce the likelihood of errors.
Plugins:
Jenkins supports over 1,500 plugins that allow it to integrate with a variety of tools and services such as Git, Maven, Gradle, Docker, Kubernetes, and many others.
Support for various languages and technologies:
Jenkins supports many programming languages and technologies, making it a versatile tool for CI/CD.
Scalability:
Jenkins can be configured to run in a distributed environment using agents (nodes) to run tasks on different machines.
Integration with version control systems:
Jenkins can integrate with various version control systems such as Git, Subversion, Mercurial and others.
Web interface and API:
Jenkins provides a convenient web interface for managing and monitoring tasks, as well as a RESTful API for integration with other systems.
Example of using Jenkins for a Spring Boot project:
Installing Jenkins:
You can install Jenkins using Docker by downloading the distribution from the official website or using your operating system's package manager.
docker run -p 8080:8080 -p 50000:50000 jenkins/jenkins:lts
Jenkins setup:
Open your browser and go to http://localhost:8080.
Enter the administrator password, which can be found in the /var/jenkins_home/secrets/initialAdminPassword file inside the container.
Follow the installation wizard to complete the Jenkins setup.
Step 2: Configure Jenkins to Build Spring Boot Application
Installing plugins:
In Jenkins, go to Manage Jenkins -> Manage Plugins.
Install the following plugins:
Gradle Plugin
Git Plugin
Gradle setup:
Make sure Gradle is installed on your Jenkins server. If not, install it.
In Jenkins, go to Manage Jenkins -> Global Tool Configuration.
In the Gradle section, add a new Gradle installation, specifying the name and path to Gradle (or select automatic installation).
Creating a new task (Job):
In Jenkins, go to the home page and click New Item.
Enter a project name and select Freestyle project.
Click OK.
Setting up the code source:
In the Source Code Management section, select Git.
Please provide your project repository URL and access credentials.
Build setup:
In the Build section, click Add build step and select Invoke Gradle script.
In the Switches field, enter clean build.
In the Tasks field, enter build.
Setting up build triggers:
In the Build Triggers section, select how Jenkins will trigger the build:
Poll SCM: To poll the repository for changes.
Build periodically: To run builds on a schedule.
GitHub hook trigger for GITScm polling: To automatically trigger a build when pushing to a repository.
Setting up post-build actions:
You can add post-build actions such as sending notifications or deploying the app.
➤ What is Nexus
Nexus is a repository manager used to store, manage, and distribute artifacts such as libraries, packages, and other components used in the software development process. It supports many formats, including Maven, npm, NuGet, Docker, and many others.
Key features of Nexus:
Storage of artifacts:
Nexus provides a centralized place to store all artifacts used in the development process, such as libraries, plugins, and other dependencies.
Dependency Management:
Nexus helps manage project dependencies by providing access to the required artifacts and their versions.
Caching remote repositories:
Nexus can cache artifacts from remote repositories, which helps reduce latency and improve performance.
Support for multiple formats:
Nexus supports various repository formats such as Maven, npm, NuGet, Docker and others.
Access control:
Nexus provides a flexible access control system, allowing you to control who can upload and download artifacts.
Integration with CI/CD:
Nexus easily integrates with continuous integration and deployment (CI/CD) tools like Jenkins to automate build and deployment processes.
You can install Nexus using Docker:
docker run -d -p 8081:8081 --name nexus sonatype/nexus3
Once the Nexus container is running, open a browser and navigate to http://localhost:8081. Use the following default login details:
Login: admin
Password: You can find the password in the admin.password file located in the /nexus-data/admin.password directory inside the container.
➤ What is a Query Plan?
A query plan is a set of instructions created by a database management system (DBMS) that describe how the DBMS will execute a SQL query. A query plan contains information about the order in which operations are performed, the indexes used, the data access methods, and other aspects that affect the performance of a query.
The main components of a query plan are:
Sequence of operations:
Operations may include table scans, using indexes, joining tables, sorting, and aggregating data.
Used indices:
A query plan specifies which indexes are used to speed up access to data.
Data access methods:
Methods may include full table scans, index scans, primary key lookups, etc.
The order of joining tables:
A query plan describes the order in which tables are joined and the join methods (e.g. nested loops, hash join, merge).
In PostgreSQL, you can use the EXPLAIN command to get a query plan:
EXPLAIN SELECT * FROM users WHERE id = 1;
Example of a query plan:
Seq Scan on users (cost=0.00..35.50 rows=1 width=64) Filter: (id = 1)
Seq Scan:
Specifies a sequential scan of the users table.
cost=0.00..35.50:
Estimated cost of executing the request. The first value (0.00) is the start cost, and the second (35.50) is the total cost of executing the request.
rows=1:
The expected number of rows returned by the query.
width=64:
Expected average row size in bytes.
Filter:
Indicates a filtering condition (id = 1).
Optimizing queries using a query plan:
Analyzing the query plan allows you to identify bottlenecks and take steps to optimize queries. Here are some examples of optimizations:
Using indexes:
Create indexes on columns used in WHERE, JOIN, and ORDER BY conditions.
Avoiding full table scan:
Using indexed scans instead of full table scans to improve performance.
Optimizing table connections:
Selecting a suitable table join method (e.g. hash join, merge) depending on the data volume.\
Query refactoring:
Rewriting complex queries to reduce the number of operations and improve performance.
Example of using index
Creating an index:
CREATE INDEX idx_users_id ON users (id);
Getting a query plan after creating an index:
EXPLAIN SELECT * FROM users WHERE id = 1;
Example of a query plan after creating an index:
Index Scan using idx_users_id on users (cost=0.28..8.30 rows=1 width=64) Index Cond: (id = 1)
Index Scan:
Specifies the use of indexed scanning on the idx_users_id index.
cost=0.28..8.30:
Estimated query execution cost, which is significantly reduced compared to a sequential table scan.
Index Cond:
Indicates the condition for using the index (id = 1).
➤ How to Use Indexes in Spring Database
Example of creating an index in Spring Data JPA
dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.h2database:h2' implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' implementation 'org.jetbrains.kotlin:kotlin-reflect' implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' testImplementation 'org.springframework.boot:spring-boot-starter-test' }
spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true
Use the @Table and @Index annotations to create entity-level indexes.
import javax.persistence.* @Entity @Table(name = "users", indexes = [Index(name = "idx_user_email", columnList = "email")]) data class User( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, @Column(nullable = false) val name: String, @Column(nullable = false, unique = true) val email: String )
Example SQL query to check index:
SHOW INDEX FROM users;
➤ What anomalies can occur when working with a database
Database anomalies typically occur in the context of transactional operations and concurrent data access. These anomalies can lead to unpredictable behavior and incorrect results.
Major types of anomalies include:
Dirty Read
Non-Repeatable Read
Phantom Read
Lost Update
Double Update Anomaly
Dirty Read:
Occurs when one transaction reads data that has been modified by another transaction but not yet committed (completed). If the second transaction is rolled back, the first transaction contains invalid data.
Example:
Transaction A updates the value of the column but does not commit the changes.
Transaction B reads the updated value.
Transaction A is rolled back.
Transaction B is left with incorrect data.
Non-repeatable Read:
Occurs when one transaction reads the same record twice, and another transaction modifies or deletes that record between those two reads. As a result, the first and second reads of the same record return different results.
Example:
Transaction A reads the column value.
Transaction B updates the column value and commits the changes.
Transaction A reads the column value again and sees a different value.
Phantom Read:
Occurs when one transaction executes the same query twice to select a set of rows, and another transaction adds or deletes rows that match the query criteria between these queries. As a result, the first and second queries return different sets of rows.
Example:
Transaction A executes a query that returns a rowset.
Transaction B inserts a new row that matches the conditions of transaction A's request and commits the changes.
Transaction A runs the same query again and sees an additional row.
Lost Update:
Occurs when two transactions simultaneously read the same record and then update it. The last update overwrites the previous one, and the first transaction's changes are lost.
Example:
Transaction A reads the column value.
Transaction B reads the same column value.
Transaction A updates the value and commits the changes.
Transaction B updates the value and commits the changes, overwriting the changes made by transaction A.
Double Update Anomaly:
This anomaly occurs when the same update is applied twice to the same row due to transaction re-execution.
Example:
Transaction A increments the value of the column by 1.
Transaction A fails and is automatically retried.
The column value is increased by 2 instead of 1.
Uncommitted Dependency:
Occurs when one transaction hangs because it is waiting for another transaction to complete, which holds a lock on a required resource. This can result in poor performance and poor system responsiveness.
Cross-updates (Write Skew):
Occurs when two transactions simultaneously modify different but related data, resulting in an inconsistent state of the system. For example, if two transactions check conditions and update data based on them, a consistency violation may occur if the conditions are checked before the data is updated.
Managing anomalies using isolation levels:
Different transaction isolation levels are defined by the SQL standard to manage these anomalies.
Isolation.READ UNCOMMITTED:
The lowest isolation level. Allows dirty reads, non-repeatable reads, and phantom reads.
Isolation.READ COMMITTED:
Disallows dirty reads. Allows non-repeatable reads and phantom reads.
Isolation.REPEATABLE READ:
Disallows dirty reads and non-repeatable reads. Allows phantom reads.
Isolation.SERIALIZABLE:
The highest level of isolation. Disallows dirty reads, non-repeatable reads, and phantom reads.
Example of setting the isolation level in Spring:
import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Isolation import org.springframework.transaction.annotation.Transactional @Service class MyService { @Transactional(isolation = Isolation.SERIALIZABLE) fun performTransactionalOperation() { } }
➤ What are the types of locking in the database
By blocking modality:
Shared Locks:
allows multiple transactions to read data simultaneously. However, none of these transactions can modify the data while the shared lock is held.
Example:
Transaction A takes a Shared Lock on the row.
Transaction B can also take a Shared Lock on the same row and read the data.
Transaction C cannot take an Exclusive Lock on this row while the Shared Locks of transactions A and B are held.
-- Transaction A takes a shared lock BEGIN TRANSACTION; SELECT * FROM MyTable WITH (HOLDLOCK, ROWLOCK);
import org.springframework.data.jpa.repository.Lock import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.CrudRepository import javax.persistence.LockModeType interface MyRepository : CrudRepository<MyEntity, Long> { @Lock(LockModeType.PESSIMISTIC_READ) @Query("SELECT e FROM MyEntity e WHERE e.id = :id") fun findWithSharedLock(id: Long): MyEntity? }
Exclusive Locks:
Allows a transaction to both read and modify data. Only one transaction can have an exclusive lock on a resource, and no other transaction can take a shared or exclusive lock on that resource while the exclusive lock is held.
Example:
Transaction A takes an Exclusive Lock on a row.
Transaction B cannot take a Shared Lock or Exclusive Lock on this row while Transaction A's Exclusive Lock is held.
-- Transaction B takes an exclusive lock BEGIN TRANSACTION; UPDATE MyTable WITH (ROWLOCK) SET Column = 'Value';
import org.springframework.data.jpa.repository.Lock import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.CrudRepository import javax.persistence.LockModeType interface MyRepository : CrudRepository<MyEntity, Long> { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT e FROM MyEntity e WHERE e.id = :id") fun findWithExclusiveLock(id: Long): MyEntity? }
An example of using locks in Spring transactions:
import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import javax.persistence.EntityManager import javax.persistence.LockModeType @Service class MyService @Autowired constructor( private val myRepository: MyRepository, private val entityManager: EntityManager ) { @Transactional fun performSharedLockOperation(id: Long) { val entity = myRepository.findWithSharedLock(id) // Your code is here } @Transactional fun performExclusiveLockOperation(id: Long) { val entity = myRepository.findWithExclusiveLock(id) // Your code is here } @Transactional fun performExclusiveLockOperationWithEntityManager(id: Long) { val entity = entityManager.find(MyEntity::class.java, id, LockModeType.PESSIMISTIC_WRITE) // Your code is here } }
By level of data affected:
Row Lock:
Locks a single row in a table. This allows multiple users to work on different rows of the same table at the same time.
Page Lock:
Locks a data page containing multiple rows. Effective for bulk data operations, but may result in more contention than row locking.
Table Lock:
Locks the entire table. This prevents any other operations on the table while the lock is active. Use when you need to perform operations that affect most or all of the table.
By control method:
Optimistic blocking:
An approach in which the system assumes that conflicts are unlikely and does not acquire a lock until the changes are committed. Instead, when attempting to commit changes, it checks whether the data has been modified since it was last read.
Pessimistic blocking:
An approach in which the system assumes that conflicts are likely and places locks on data while it is being read, holding them until the transaction ends. This minimizes the risk of conflicts, but can result in decreased performance and deadlocks.
There are also:
Intent Lock:
Used to indicate intent to acquire locks at lower levels. For example, if a transaction intends to lock rows within a page, it will first acquire an intent lock at the page level.
Deadlock:
A deadlock occurs when two or more transactions are waiting for another to release a resource they need. DBMSs typically detect deadlocks and automatically interrupt one of the transactions to resolve the conflict.
➤ What are the ways to manage transactions in Spring
In the Spring Framework, transaction management can be implemented in several ways. The main ways include declarative and programmatic transaction management.
Declarative transaction management:
@Transactional:
Used to specify methods or classes that should be executed within a transaction.
You can configure parameters such as isolation level, propagation, timeout, etc.
import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service class MyService { @Transactional fun executeTransactionalMethod() { // Here the code will be executed within the transaction. } }
XML configuration:
Configuration of transaction manager and aspects in XML is possible.
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:advice id="txAdvice"> <tx:attributes> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="serviceOperation" expression="execution(* com.example.service.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/> </aop:config>
Software Transaction Management:
Programmatic transaction management involves explicitly using PlatformTransactionManager to manage transactions in your code.
import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import org.springframework.transaction.PlatformTransactionManager import org.springframework.transaction.TransactionDefinition import org.springframework.transaction.TransactionStatus import org.springframework.transaction.support.DefaultTransactionDefinition @Service class MyTransactionalService(@Autowired private val transactionManager: PlatformTransactionManager) { fun executeTransactionalOperation() { val transactionDefinition = DefaultTransactionDefinition() transactionDefinition.name = "exampleTransaction" transactionDefinition.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED val transactionStatus: TransactionStatus = transactionManager.getTransaction(transactionDefinition) try { // Executing business logic // For example, inserting data into a database transactionManager.commit(transactionStatus) } catch (ex: Exception) { transactionManager.rollback(transactionStatus) throw ex } } }
Ways to manage transactions depending on the context:
JDBC transactions:
Used by DataSourceTransactionManager.
Suitable for transaction management at the JDBC level.
@Bean fun transactionManager(dataSource: DataSource): PlatformTransactionManager { return DataSourceTransactionManager(dataSource) }
JPA transactions:
JpaTransactionManager is used.
Suitable for transaction management at the JPA level.
@Bean fun transactionManager(entityManagerFactory: EntityManagerFactory): PlatformTransactionManager { return JpaTransactionManager(entityManagerFactory) }
Hibernate transactions:
HibernateTransactionManager is used.
Suitable for transaction management at the Hibernate level.
@Bean fun transactionManager(sessionFactory: SessionFactory): PlatformTransactionManager { return HibernateTransactionManager(sessionFactory) }
JTA transactions:
JtaTransactionManager is used.
Suitable for managing distributed transactions in enterprise applications.
@Bean fun transactionManager(): PlatformTransactionManager { return JtaTransactionManager() }
TransactionOperations and TransactionTemplate:
Provides programmatic transaction management with simplified syntax.
TransactionOperations is an interface in Spring that defines the basic operations for working with transactions. TransactionTemplate is a concrete implementation of this interface.
Main methods of TransactionOperations:
<T> execute(TransactionCallback<T> action):
Executes a transaction and returns the result.
<T> executeWithoutResult(TransactionCallbackWithoutResult action):
Executes a transaction without returning a result.
TransactionStatus:
Provides methods for managing the current state of a transaction. This object is passed to the transaction callback method and can be used to control the progress of the transaction, including setting a savepoint, rolling back the transaction, and checking its status.
Main methods of TransactionStatus:
setRollbackOnly():
Marks the current transaction for rollback. After calling this method, the transaction will be rolled back on completion, even if the method succeeds.
isNewTransaction():
Returns true if the current transaction status is a new transaction and not an existing one.
hasSavepoint():
Returns true if any savepoint is set in the current transaction.
createSavepoint():
Creates a new savepoint in the current transaction. A savepoint allows you to roll back part of a transaction without rolling back the entire transaction.
releaseSavepoint(Object savepoint):
Frees (deletes) the specified savepoint.
rollbackToSavepoint(Object savepoint):
Rolls back the transaction to the specified savepoint.
import org.springframework.stereotype.Service import org.springframework.transaction.support.TransactionCallback import org.springframework.transaction.support.TransactionCallbackWithoutResult import org.springframework.transaction.support.TransactionTemplate import org.springframework.transaction.TransactionStatus @Service class MyService(private val transactionTemplate: TransactionTemplate) { fun executeInTransaction() { transactionTemplate.execute(object : TransactionCallbackWithoutResult() { override fun doInTransactionWithoutResult(status: TransactionStatus) { println("Executing in transaction") // If needed, you can call status.setRollbackOnly() to roll back the transaction. status.rollbackToSavepoint(savepoint) } }) } fun executeInTransactionWithResult(): String { return transactionTemplate.execute(TransactionCallback { status -> println("Executing in transaction with result") "Transaction Result" }) ?: "Default Result" } }
TransactionSynchronizationManager:
Provides low-level access to control transaction synchronization.
import org.springframework.transaction.support.TransactionSynchronizationManager class MyService { fun someMethod() { if (TransactionSynchronizationManager.isActualTransactionActive()) { // The current transaction is active } } }
➤ What is R2DBC (Reactive Relational Database Connectivity)
To work with a database in a reactive style in WebFlux, you can use the Spring Data R2DBC (Reactive Relational Database Connectivity) project. This project allows you to work with relational databases in a reactive style.
dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc' implementation 'io.r2dbc:r2dbc-postgresql' }
spring: r2dbc: url: r2dbc:postgresql://localhost:5432/mydatabase username: myuser password: mypassword
import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Table @Table("users") data class User( @Id val id: Long? = null, val name: String, val email: String )
import org.springframework.data.repository.reactive.ReactiveCrudRepository import reactor.core.publisher.Mono interface UserRepository : ReactiveCrudRepository<User, Long> { fun findByEmail(email: String): Mono<User> }
import org.springframework.stereotype.Service import reactor.core.publisher.Flux import reactor.core.publisher.Mono @Service class UserService(private val userRepository: UserRepository) { fun getAllUsers(): Flux<User> = userRepository.findAll() fun getUserById(id: Long): Mono<User> = userRepository.findById(id) fun createUser(user: User): Mono<User> = userRepository.save(user) fun updateUser(id: Long, user: User): Mono<User> = userRepository.findById(id) .flatMap { val updatedUser = it.copy(name = user.name, email = user.email) userRepository.save(updatedUser) } fun deleteUser(id: Long): Mono<Void> = userRepository.deleteById(id) }
import org.springframework.web.bind.annotation.* import reactor.core.publisher.Flux import reactor.core.publisher.Mono @RestController @RequestMapping("/users") class UserController(private val userService: UserService) { @GetMapping fun getAllUsers(): Flux<User> = userService.getAllUsers() @GetMapping("/{id}") fun getUserById(@PathVariable id: Long): Mono<User> = userService.getUserById(id) @PostMapping fun createUser(@RequestBody user: User): Mono<User> = userService.createUser(user) @PutMapping("/{id}") fun updateUser(@PathVariable id: Long, @RequestBody user: User): Mono<User> = userService.updateUser(id, user) @DeleteMapping("/{id}") fun deleteUser(@PathVariable id: Long): Mono<Void> = userService.deleteUser(id) }
Asynchronous libraries:
Make sure you are using asynchronous drivers for your database. In the case of PostgreSQL, this is r2dbc-postgresql.
Error handling:
It is important to consider error handling and transaction management in reactive applications.
➤ What is Flyway, migrations
A popular tool for database versioning management. It allows you to organize database migrations, i.e. a sequence of changes (e.g. creating tables, changing the schema, filling with data, etc.), and automate their application.
Main features of Flyway:
Versioning of migrations:
Each migration is identified by a unique version number, allowing you to track changes to the database and ensure that they are applied in the correct order.
Support for various databases:
Flyway supports many databases such as PostgreSQL, MySQL, Oracle, SQL Server and many others.
Integration with various tools:
Flyway easily integrates with popular project builders and frameworks such as Maven, Gradle, Spring Boot, etc.
Automatic detection and execution of migrations:
Flyway automatically detects new migrations and applies them to the database.
Support for different types of migrations:
Flyway supports SQL and Java migrations, allowing you to flexibly manage changes in your database.
dependencies { implementation 'org.flywaydb:flyway-core' }
logging: level: org.springframework.data.r2dbc: DEBUG spring: r2dbc: url: r2dbc:postgresql://localhost:5432/database username: user password: 123 flyway: url: jdbc:postgresql://localhost:5432/database user: user password: 123 locations: classpath:db/migration jwt: secret: very-very-secret-key-should-be-almost-infinity expiration: 86400
Where
logging: level: org.springframework.data.r2dbc: DEBUG
sets the logging level for the org.springframework.data.r2dbc package to DEBUG. This means that detailed messages about database operations performed via R2DBC will be output.
spring: r2dbc: url: r2dbc:postgresql://localhost:5432/database username: user password: 123
The parameters configure the connection to the PostgreSQL database using R2DBC (Reactive Relational Database Connectivity):
spring.r2dbc.url:
URL for connecting to the database via R2DBC.
spring.r2dbc.username:
Username to connect to the database.
spring.r2dbc.password:
Password to connect to the database.
flyway: url: jdbc:postgresql://localhost:5432/database user: user password: 123 locations: classpath:db/migration
The parameters configure Flyway to manage database migrations:
spring.flyway.url:
URL for connecting to the database via JDBC.
spring.flyway.user:
Username to connect to the database.
spring.flyway.password:
Password to connect to the database.
It is important to note that Flyway uses JDBC to perform migrations, even if the application itself uses R2DBC to work with the database.
jwt: secret: very-very-secret-key-should-be-almost-infinity expiration: 86400
The parameters configure JWT for authentication and authorization:
jwt.secret:
Secret key for signing JWT. This key must be strong and secret to ensure the security of the tokens.
jwt.expiration:
Token lifetime in seconds. In this case, the token will be valid for 86400 seconds (24 hours).
Migrations must be located in the src/main/resources/db/migration directory and follow a specific naming format: V__.sql
Example of migration V1__Create_user_table.sql:
CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(100) NOT NULL UNIQUE );
Flyway will automatically run migrations when your Spring Boot application starts. If new migrations are found in the database, they will be applied.
➤ Where should private data be stored in a Spring application?
In Spring applications, private keys and other sensitive data must be stored securely to prevent unauthorized access. Here are some recommendations for storing them:
Spring Boot Configuration Properties:
Use the application.properties or application.yml file, but do not store the keys themselves in them, just specify the paths to them.
Environment Variables:
Environment variables are a secure way to store sensitive information.
@Value("\${my.secret.key}") private lateinit var mySecretKey: String
External Configuration Server:
Use Spring Cloud Config Server, which allows you to centrally manage configuration and store keys securely. Data can be encrypted and protected.
Vault:
HashiCorp Vault is a powerful solution for storing secrets. Spring Cloud Vault allows you to integrate Vault with your application.
spring: cloud: vault: uri: http://127.0.0.1:8200 token: s.xxxxxxxx kv: enabled: true backend: secret default-context: application
Java Keystore (JKS):
Storing keys in JKS is a good practice. Example of loading a key from JKS storage:
import java.io.FileInputStream import java.security.KeyStore val keyStore = KeyStore.getInstance("JKS") val fis = FileInputStream("keystore.jks") keyStore.load(fis, "keystore-password".toCharArray()) val privateKey = keyStore.getKey("alias", "key-password".toCharArray()) as PrivateKey
AWS Secrets Manager / Azure Key Vault / Google Secret Manager:
If your application is deployed in the cloud, use cloud services to store secrets.
import com.amazonaws.services.secretsmanager.AWSSecretsManager import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest @Autowired private lateinit var awsSecretsManager: AWSSecretsManager fun getSecret(): String { val getSecretValueRequest = GetSecretValueRequest().withSecretId("mySecretId") val getSecretValueResult = awsSecretsManager.getSecretValue(getSecretValueRequest) return getSecretValueResult.secretString }
Data encryption:
Regardless of where it is stored, the data should be encrypted. Use libraries such as Jasypt to encrypt the data.
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class JasyptConfig { @Bean fun stringEncryptor(): PooledPBEStringEncryptor { val encryptor = PooledPBEStringEncryptor() val config = SimpleStringPBEConfig().apply { password = "encryption-password" algorithm = "PBEWithMD5AndDES" } encryptor.setConfig(config) return encryptor } }
➤ How to use caching in Spring
Cache configuration:
Enabling caching in a configuration class using the @EnableCaching annotation.
Configuring cache in resource files (e.g. application.yml).
Using caching annotations:
Annotations such as @Cacheable, @CachePut, @CacheEvict, and @Caching indicate which methods should be cached, updated, or cleared in the cache.
Annotation processing:
Spring processes these annotations using Proxies or Aspects, adding additional cache handling code before calling methods.
Working with cache:
The first time a method marked with the @Cacheable annotation is called, the original method is executed and the result is stored in the cache.
On subsequent calls to the same method with the same parameters, the result is retrieved from the cache, bypassing the execution of the original method.
Updating and deleting data in the cache:
The @CachePut annotation updates the data in the cache after the method is executed.
The @CacheEvict annotation removes data from the cache to ensure that stale data is not returned.
Example of cache operation:
Let's consider how caching works using the getUserById method as an example.
First call to getUserById(1) method:
The method is executed.
The result (User(id=1, name=”User1”)) is saved to the cache with the key
Subsequent call to the getUserById(1) method:
Spring checks if the result is in the cache.
If there is a result, it is returned from the cache and the method is not executed again.
Updating data using updateUser(User(1, “UpdatedUser1”)):
The method is executed.
The cache is updated with a new value (User(id=1, name=”UpdatedUser1”)) with key 1.
Deleting data from cache using deleteUser(1):
The method is executed.
The entry with key 1 is removed from the cache.
dependencies { implementation 'org.springframework.boot:spring-boot-starter-cache' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.jetbrains.kotlin:kotlin-reflect' implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.6.1' runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' }
import org.springframework.cache.annotation.EnableCaching import org.springframework.context.annotation.Configuration @Configuration @EnableCaching class CacheConfig
@Cacheable:
Caches the result of the method.
@CachePut:
Updates the cache after executing the method.
@CacheEvict:
Removes entries from the cache.
@Service class UserService { @Cacheable("users", key = "#id") fun getUserById(id: Long): User { // Simulating a long query Thread.sleep(3000) return User(id, "User$id") } @CachePut(value = ["users"], key = "#user.id") fun updateUser(user: User): User { // User update logic return user } @CacheEvict(value = ["users"], key = "#id") fun deleteUser(id: Long) { // Logic for deleting a user } @Caching( put = [CachePut(value = ["users"], key = "#user.id")], evict = [CacheEvict(value = ["users_list"], allEntries = true)] ) fun saveUser(user: User): User { // Logic for saving user return user } }
Possible cache configurations:
Simple (ConcurrentMapCache):
This is the default cache type, which uses ConcurrentHashMap to store the cache in memory.
JCache (JSR-107):
It is a Java standard for caching that allows integration with various cache providers such as Ehcache, Hazelcast, Infinispan and others.
Ehcache:
A popular and powerful cache provider that supports advanced cache management features.
Hazelcast:
A clustered distributed cache that can also be used as a network game server.
Infinispan:
A scalable and distributed cache provider that can run both in-memory and on-disk.
Caffeine:
A high-performance cache for Java that supports configurable expiration policy and other features.
Redis:
A distributed in-memory data store that can be used for caching and messaging.
Guava:
A library from Google that provides a basic caching mechanism.
Simple Cache (ConcurrentMapCache) application.yml:
spring: cache: type: simple
JCache (JSR-107) application.yml:
spring: cache: type: jcache
Ehcache application.yml:
spring: cache: type: ehcache
Ehcache ehcache.xml:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ehcache.xsd"> <cache name="users" maxEntriesLocalHeap="1000" timeToLiveSeconds="3600"/> </ehcache>
Hazelcast application.yml:
spring: cache: type: hazelcast
Hazelcast hazelcast.xml:
<hazelcast xmlns="http://www.hazelcast.com/schema/config" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.hazelcast.com/schema/config http://www.hazelcast.com/schema/config/hazelcast-config-3.10.xsd"> <map name="users"> <time-to-live-seconds>3600</time-to-live-seconds> </map> </hazelcast>
Infinispan application.yml:
spring: cache: type: infinispan
Infinispan infinispan.xml:
<infinispan> <cache-container> <local-cache name="users"> <expiration lifespan="3600000"/> </local-cache> </cache-container> </infinispan>
Caffeine application.yml:
spring: cache: type: caffeine
import com.github.benmanes.caffeine.cache.Caffeine import org.springframework.cache.CacheManager import org.springframework.cache.caffeine.CaffeineCacheManager import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import java.util.concurrent.TimeUnit @Configuration class CacheConfig { @Bean fun cacheManager(): CacheManager { val cacheManager = CaffeineCacheManager("users") cacheManager.setCaffeine( Caffeine.newBuilder() .expireAfterWrite(60, TimeUnit.MINUTES) .maximumSize(100) ) return cacheManager } }
Redis application.yml:
spring: cache: type: redis redis: host: localhost port: 6379
import org.springframework.cache.annotation.EnableCaching import org.springframework.cache.redis.RedisCacheConfiguration import org.springframework.cache.redis.RedisCacheManager import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.data.redis.connection.RedisConnectionFactory import org.springframework.data.redis.core.RedisTemplate import org.springframework.data.redis.serializer.RedisSerializationContext import org.springframework.data.redis.serializer.StringRedisSerializer @Configuration @EnableCaching class CacheConfig { @Bean fun redisCacheManager(redisConnectionFactory: RedisConnectionFactory): RedisCacheManager { val config = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer())) return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(config) .build() } @Bean fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<String, Any> { val template = RedisTemplate<String, Any>() template.setConnectionFactory(redisConnectionFactory) template.keySerializer = StringRedisSerializer() template.valueSerializer = StringRedisSerializer() return template } }
Guava application.yml:
spring: cache: type: guava
import com.google.common.cache.CacheBuilder import org.springframework.cache.CacheManager import org.springframework.cache.guava.GuavaCacheManager import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import java.util.concurrent.TimeUnit @Configuration class CacheConfig { @Bean fun cacheManager(): CacheManager { val cacheManager = GuavaCacheManager("users") cacheManager.setCacheBuilder( CacheBuilder.newBuilder() .expireAfterWrite(60, TimeUnit.MINUTES) .maximumSize(100) ) return cacheManager } }
➤ How to use AWS (Amazon Web Services) in Spring
Spring Boot integration with AWS (Amazon Web Services) can include many different services such as Amazon S3, Amazon RDS, Amazon SQS, and more. In this example, we will look at how to integrate with Amazon S3 to upload and download files.
Example of Spring Boot integration with Amazon S3:
dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("software.amazon.awssdk:s3") implementation("software.amazon.awssdk:auth") }
aws.accessKeyId=YOUR_ACCESS_KEY_ID aws.secretKey=YOUR_SECRET_KEY aws.region=us-east-1 aws.s3.bucketName=your-bucket-name
import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import software.amazon.awssdk.auth.credentials.AwsBasicCredentials import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.s3.S3Client @Configuration class AwsS3Config { @Value("\${aws.accessKeyId}") lateinit var accessKeyId: String @Value("\${aws.secretKey}") lateinit var secretKey: String @Value("\${aws.region}") lateinit var region: String @Bean fun s3Client(): S3Client { val credentials = AwsBasicCredentials.create(accessKeyId, secretKey) return S3Client.builder() .region(Region.of(region)) .credentialsProvider(StaticCredentialsProvider.create(credentials)) .build() } }
import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import org.springframework.web.multipart.MultipartFile import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.model.PutObjectRequest import software.amazon.awssdk.services.s3.model.GetObjectRequest import java.nio.file.Paths @Service class S3Service(private val s3Client: S3Client) { @Value("\${aws.s3.bucketName}") lateinit var bucketName: String fun uploadFile(file: MultipartFile): String { val fileName = file.originalFilename ?: throw IllegalArgumentException("File name is missing") val putObjectRequest = PutObjectRequest.builder() .bucket(bucketName) .key(fileName) .build() s3Client.putObject(putObjectRequest, Paths.get(fileName)) return "File uploaded successfully: $fileName" } fun downloadFile(fileName: String): ByteArray { val getObjectRequest = GetObjectRequest.builder() .bucket(bucketName) .key(fileName) .build() return s3Client.getObject(getObjectRequest).readAllBytes() } }
import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/s3") class S3Controller(private val s3Service: S3Service) { @PostMapping("/upload") fun uploadFile(@RequestParam("file") file: MultipartFile): String { return s3Service.uploadFile(file) } @GetMapping("/download/{fileName}") fun downloadFile(@PathVariable fileName: String): ResponseEntity<ByteArray> { val fileBytes = s3Service.downloadFile(fileName) return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"$fileName\"") .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(fileBytes) } }
➤ How to set up GitHub Actions Continuous Integration
Continuous Integration (CI) is a software development practice in which developers frequently integrate changes into a master code repository. Each integration is verified by automated builds and tests, allowing errors to be detected and fixed early.
Example of CI setup using GitHub Actions for a Spring Boot project:
Creating a repository on GitHub:
Create a new repository on GitHub.
Clone the repository to your local machine and add the source code of your Spring Boot project.
Adding a GitHub Actions configuration file:
At the root of your project, create a directory .github/workflows and inside it create a file ci.yml.
.github/workflows/ci.yml
name: CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v2 - name: Set up JDK 11 uses: actions/setup-java@v2 with: distribution: 'adopt' java-version: '11' - name: Cache Gradle packages uses: actions/cache@v2 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle run: ./gradlew build - name: Run tests run: ./gradlew test
name: CI:
Workflow name.
on:
Defines triggers for running the workflow. In this case, it is push and pull_request to the main branch.
jobs:
Defines a set of tasks to be performed.
build:
The name of the task to be executed.
runs-on: ubuntu-latest:
Specifies that the task will be run on the latest version of Ubuntu.
steps:
Defines the steps to complete the task:
Checkout repository:
Cloning a repository.
Set up JDK 11:
Installing JDK 11.
Cache Gradle packages:
Caching Gradle dependencies to speed up subsequent builds.
Grant execute permission for gradlew:
Granting execute permissions to the gradlew file.
Build with Gradle:
Building a project using Gradle.
Run tests:
Running tests.