Перейти к содержимому

Вопросы и ответы — Java / Spring Developer — Хранение данных

➤ Что такое ACID (Atomicity, Consistency, Isolation, Durability)
➤ Что такое CRUD (Create, Read, Update, Delete)
➤ Что такое JPA (Java Persistence API) и Hibernate
➤ Что такое Spring Data JPA
➤ Что такое JDBC (Java Database Connectivity)
➤ Чем JPA (Java Persistence API) отличается от JDBC (Java Database Connectivity)
➤ Что такое JPQL и Criteria API
➤ Как подключаться к базам данных в Spring
➤ Какие есть уровни изоляции транзакций
➤ Какие есть параметры у аннотации @Transactional
➤ Как использовать Redis в Spring
➤ Как в Spring обрабатывать изменения в базе данных
➤ Какие есть репозитории в Spring
➤ Что такое Jenkins
➤ Что такое Nexus
➤ Что такое План запроса (Query Plan)
➤ Как использовать индексы в базе данных Spring
➤ Какие могут быть аномалии при работе с базой данных
➤ Какие есть виды блокировки в базе данных
➤ Что такое ACID (Atomicity, Consistency, Isolation, Durability)
➤ Что такое CRUD (Create, Read, Update, Delete)
➤ Что такое JPA (Java Persistence API) и Hibernate
➤ Что такое Spring Data JPA
➤ Что такое JDBC (Java Database Connectivity)
➤ Чем JPA (Java Persistence API) отличается от JDBC (Java Database Connectivity)
➤ Что такое JPQL и Criteria API
➤ Как подключаться к базам данных в Spring
➤ Какие есть уровни изоляции транзакций
➤ Какие есть параметры у аннотации @Transactional
➤ Как использовать Redis в Spring
➤ Как в Spring обрабатывать изменения в базе данных
➤ Какие есть репозитории в Spring
➤ Что такое Jenkins
➤ Что такое Nexus
➤ Что такое План запроса (Query Plan)
➤ Как использовать индексы в базе данных Spring
➤ Какие могут быть аномалии при работе с базой данных
➤ Какие есть виды блокировки в базе данных
➤ Какие есть способы управления транзакциями в Spring
➤ Что такое R2DBC (Reactive Relational Database Connectivity)
➤ Что такое Flyway, миграции
➤ Где в Spring приложении следует хранить приватные данные
➤ Как в Spring можно использовать кеширование
➤ Как использовать AWS / Amazon Web Services в Spring
➤ Как настроить GitHub Actions Continuous Integration

Набор свойств, которые гарантируют надёжность и предсказуемость транзакций в системах управления базами данных (СУБД). Аббревиатура расшифровывается как:

Транзакция выполняется целиком или не выполняется вообще.
Если произошла ошибка на любом этапе, система откатывает все изменения, внесённые в рамках этой транзакции.

Пример:
если вы переводите деньги с одного счёта на другой, операция списания и операция зачисления должны пройти вместе. Если зачисление не удалось, то и списание отменяется.

После завершения транзакции база должна оставаться в корректном и согласованном состоянии.
Это означает соблюдение всех ограничений (уникальность, внешние ключи, правила и т. д.).

Пример:
если сумма на счёте не может быть отрицательной, то ни одна транзакция не должна привести к нарушению этого правила.

Одновременные транзакции не должны мешать друг другу.
Каждая транзакция должна выполняться так, будто она одна в системе.
СУБД использует уровни изоляции (Read Uncommitted, Read Committed, Repeatable Read, Serializable), чтобы управлять этим свойством.

Пример:
два пользователя не должны видеть промежуточные результаты друг друга.

После успешного завершения транзакции её изменения не будут потеряны, даже если произойдёт сбой (например, отключение электричества).
СУБД записывает изменения в журнал или на диск, чтобы обеспечить восстановление.

Пример:
если операция перевода завершена, деньги точно «уйдут» и «придут» — даже при падении сервера.

Вместе эти свойства делают возможным надёжное управление данными, особенно в условиях высокой нагрузки или сбоев. Они — основа транзакционной логики в большинстве реляционных баз данных (PostgreSQL, MySQL, Oracle и др.).

CRUD — это аббревиатура, обозначающая основные операции, которые можно выполнять с данными в базе данных. CRUD расшифровывается как Create, Read, Update, Delete:

Create (Создание):
Операция добавления новых данных в базу данных. В контексте Spring Data это может быть метод save, который сохраняет новую сущность в базе данных.

Read (Чтение):
Операция получения данных из базы данных. В Spring Data это методы типа findById, findAll и кастомные методы для поиска по различным критериям, например, findByName.

Update (Обновление):
Операция изменения существующих данных в базе данных. В Spring Data для этого также используется метод save, который обновляет сущность, если она уже существует.

Delete (Удаление):
Операция удаления данных из базы данных. В Spring Data это методы deleteById и delete.

В Spring Data есть несколько стандартных методов для работы с репозиториями. Эти методы можно использовать для базовых операций CRUD (Create, Read, Update, Delete). Примеры использования репозиториев с Kotlin и Gradle Groovy показаны ниже.

Основные стандартные методы в Spring Data репозиториях:

save(S entity: T): T:
сохраняет или обновляет сущность.

findById(id: ID): Optional:
ищет сущность по её ID.

findAll(): List:
возвращает все сущности.

deleteById(id: ID):
удаляет сущность по её ID.

delete(entity: T):
удаляет указанную сущность.

existsById(id: ID): Boolean:
проверяет существование сущности по её ID.

count(): Long:
возвращает количество сущностей.

Пример реализации, используются стандартные методы и один кастомный:

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()
    }
}

Hibernate и JPA (Java Persistence API) — это два тесно связанных термина в мире Java-проектов, связанных с объектно-реляционным отображением (ORM). Важно понимать их различия и взаимосвязь.

JPA (Java Persistence API):
JPA — это стандартная спецификация, разработанная для обеспечения ORM в приложениях Java. Она определяет интерфейсы и аннотации для управления персистентными данными и их связями. JPA не является конкретной реализацией; это просто набор правил и стандартов, которые должны быть реализованы конкретными ORM-фреймворками.

Основные характеристики JPA:
Спецификация для ORM.
Не содержит реализацию.
Определяет аннотации и интерфейсы, такие как @Entity, @Table, EntityManager, и т.д.
Обеспечивает независимость от поставщика конкретного ORM.

Hibernate:
Hibernate — это ORM-фреймворк, который является одной из реализаций спецификации JPA. Он предоставляет конкретные инструменты и механизмы для работы с базами данных на основе JPA, а также включает в себя дополнительные возможности, выходящие за рамки JPA.

Основные характеристики Hibernate:
Конкретная реализация ORM, соответствующая JPA.
Дополнительные возможности, такие как кэширование, расширенные типы ассоциаций, более гибкие критерии запросов и т.д.
Собственные аннотации и конфигурации (например, @Cache, @BatchSize).
Расширяемость и множество дополнительных инструментов для оптимизации производительности.
Взаимодействие между JPA и Hibernate
Когда вы используете JPA, вы пишете код, который следует спецификации JPA. Этот код будет работать с любой реализацией JPA, включая Hibernate, EclipseLink и другие. Hibernate, в свою очередь, предоставляет конкретные инструменты для выполнения JPA-кода, а также добавляет свои собственные возможности.

Пример использования JPA с 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);
    }
    
}

Таким образом, JPA — это спецификация для ORM, а Hibernate — это фреймворк, который реализует эту спецификацию и добавляет свои возможности.

Spring Data JPA — это часть проекта Spring Data, предназначенная для упрощения работы с базами данных на основе JPA (Java Persistence API). Это высокоуровневая библиотека, которая предоставляет абстракцию над JPA и упрощает разработку, снижая объем шаблонного кода, необходимого для выполнения основных операций с базой данных, таких как создание, чтение, обновление и удаление (CRUD). Spring Data JPA предлагает множество полезных функций, таких как автоматическая генерация запросов, поддержка методов запросов на основе имен, а также интеграция с различными механизмами хранения данных.

Основные преимущества Spring Data JPA:

Упрощение работы с базой данных:
Уменьшает количество шаблонного кода, необходимого для выполнения операций с базой данных.

Поддержка стандартных операций CRUD:
Предоставляет встроенные методы для выполнения основных операций с данными.

Автоматическая генерация запросов:
Позволяет автоматически генерировать запросы на основе имен методов репозитория.

Поддержка сложных запросов:
Поддерживает написание JPQL, Native SQL и Criteria API для более сложных запросов.

Расширяемость:
Легко расширяется и настраивается под нужды приложения.

Основные компоненты Spring Data JPA:

Repository:
Интерфейсы, которые предоставляют методы для выполнения операций с базой данных.

Entity:
Классы, представляющие сущности базы данных.

JPQL и Native Queries:
Возможность использования Java Persistence Query Language и нативных SQL-запросов для выполнения операций с базой данных.

Custom Queries:
Возможность создания пользовательских методов для выполнения специфических операций с базой данных.

Java Database Connectivity (JDBC) — это API для языка программирования Java, который определяет, как клиент может получать доступ к базе данных. JDBC является частью Java Standard Edition и предоставляет разработчикам стандартный интерфейс для взаимодействия с различными реляционными базами данных.

Основные характеристики JDBC:

Низкоуровневый API:
JDBC предоставляет низкоуровневый доступ к базам данных, что позволяет разработчикам выполнять SQL-запросы напрямую и получать результаты.

SQL-запросы:
JDBC позволяет выполнять SQL-запросы (SELECT, INSERT, UPDATE, DELETE) и другие операции с базой данных.

Управление соединениями:
JDBC управляет подключениями к базе данных, что включает открытие и закрытие соединений, обработку транзакций и управление пулом соединений.

Совместимость с различными СУБД:
JDBC поддерживает работу с различными реляционными базами данных, такими как MySQL, PostgreSQL, Oracle, SQL Server и другие.
Основные компоненты JDBC

JDBC Driver:
Драйвер JDBC является реализацией интерфейсов JDBC для конкретной базы данных. Он обеспечивает связь между Java-приложением и базой данных.

Существует четыре типа драйверов JDBC:
тип 1 (JDBC-ODBC Bridge)
тип 2 (Native API)
тип 3 (Network Protocol)
тип 4 (Thin Driver, Pure Java Driver).

Connection:
Интерфейс Connection представляет соединение с базой данных. Он используется для отправки запросов и управления транзакциями.

Statement:
Интерфейс Statement используется для выполнения статических SQL-запросов и возвращения результатов.
Существует три типа Statement: Statement, PreparedStatement (для выполнения предкомпилированных SQL-запросов) и CallableStatement (для вызова хранимых процедур).

ResultSet:
Интерфейс ResultSet представляет результирующий набор данных, возвращаемый SQL-запросом. Он позволяет перемещаться по результатам и извлекать данные.

SQLException:
Класс SQLException используется для обработки ошибок и исключений, возникающих при работе с JDBC.

Пример использования 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();
        }
    }
    
}

Java Persistence API (JPA) и Java Database Connectivity (JDBC) — это два разных подхода к работе с базами данных в Java. Вот основные различия между ними:

JDBC (Java Database Connectivity):

Уровень абстракции:
JDBC предоставляет низкоуровневый API для взаимодействия с базами данных.
Работа через JDBC требует написания SQL-запросов вручную и обработки результатов запросов через ResultSet.

Прямой доступ к базе данных:
JDBC позволяет разработчику напрямую работать с базой данных, используя SQL-запросы.
Разработчик должен сам управлять подключениями, транзакциями и обработкой ошибок.

Меньше абстракции:
JDBC предоставляет меньший уровень абстракции, что делает код более подробным и сложным для поддержки.
Требует написания большого количества кода для выполнения простых операций с базой данных.

Гибкость:
JDBC позволяет использовать все возможности конкретной СУБД, включая специфические для нее функции и расширения.

JPA (Java Persistence API):

Уровень абстракции:
JPA предоставляет более высокоуровневый API для работы с объектно-реляционным отображением (ORM).
Разработчики работают с объектами Java вместо написания SQL-запросов напрямую.

Объектно-реляционное отображение:
JPA автоматически отображает объекты Java на таблицы базы данных.
Это позволяет работать с базой данных на уровне объектов, что упрощает разработку и поддержку кода.

Управление состоянием:
JPA поддерживает управление состоянием объектов, автоматическое отслеживание изменений и синхронизацию с базой данных.
JPA автоматически обрабатывает транзакции, кеширование и другие задачи.

Меньше кода:
JPA значительно уменьшает объем кода, необходимого для выполнения операций с базой данных.
Использование аннотаций и конфигурационных файлов позволяет легко настраивать и изменять модель данных.

Кроссплатформенность:
JPA предоставляет абстракцию, которая позволяет работать с различными СУБД без изменения кода.

Пример для сравнения:

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();
}

Совместное использование JPA и JDBC:
может быть полезно в случаях, когда нужно выполнить сложные SQL-запросы, не поддерживаемые JPA, или для оптимизации производительности при работе с большими объемами данных. Вот пример, демонстрирующий, как можно использовать JPA для управления сущностями и JDBC для выполнения специфических запросов.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'com.h2database:h2' // H2 для тестирования
}
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:
Класс User представляет сущность базы данных, а интерфейс
UserRepository используется для стандартных операций CRUD с помощью JPA.

Сервисный слой:
Класс UserService содержит бизнес-логику. В методе createUser используется JPA для создания нового пользователя. Методы getAllUsersJPA и getAllUsersJDBC демонстрируют использование JPA и JDBC соответственно для получения списка пользователей.

Контроллер:
Класс UserController предоставляет REST API для взаимодействия с сервисом. Методы getUsersJPA и getUsersJDBC вызывают соответствующие методы сервиса для получения данных с использованием JPA и JDBC.

Таким образом, вы можете использовать преимущества обеих технологий: удобство и абстракцию JPA для стандартных операций и гибкость JDBC для сложных запросов.

PQL и Criteria API в Spring Data JPA:
JPQL (Java Persistence Query Language) и Criteria API предоставляют гибкие способы выполнения запросов к базе данных с использованием JPA. Оба подхода поддерживаются Spring Data JPA и могут использоваться в зависимости от требований приложения.

JPQL (Java Persistence Query Language):
JPQL — это объектно-ориентированный язык запросов, который работает с объектами JPA, а не с таблицами базы данных. Запросы на JPQL напоминают SQL, но оперируют сущностями, их атрибутами и отношениями.

Пример использования JPQL:
Рассмотрим пример, в котором мы будем использовать JPQL для выполнения запросов. В этом примере метод findByNameJPQL использует аннотацию @Query для выполнения JPQL-запроса, который находит пользователей по имени.

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:
Criteria API предоставляет типобезопасный и объектно-ориентированный способ создания запросов к базе данных. Он особенно полезен, когда запросы должны строиться динамически.

Пример использования Criteria API:
Для использования Criteria API нам нужно использовать EntityManager, который управляет операциями JPA.
Создаем интерфейс для определения кастомных методов.

interface UserRepositoryCustom {
    fun findByNameCriteria(name: String): List<User>
}

Реализуем интерфейс, используя 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
    }
    
}

Расширяем основной репозиторий кастомным интерфейсом.

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository


@Repository
interface UserRepository : JpaRepository<User, Long>, UserRepositoryCustom

В Spring Boot можно легко настроить подключение к разным типам баз данных, используя файлы конфигурации application.properties. Ниже приведены примеры настройки подключения к различным популярным базам данных: H2, MySQL, PostgreSQL, Oracle, MariaDB.

Подключение к H2 (в памяти):
H2 — это встроенная база данных, которая часто используется для разработки и тестирования.

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

Подключение к 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

Подключение к 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

Подключение к 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

Подключение к 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
}

Подключение к 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

Подключение к 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>
}

Настройка пула соединений:
Независимо от используемой базы данных, можно настроить пул соединений для улучшения производительности.

# 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

Уровни изоляции транзакций определяют степень видимости изменений данных, сделанных одной транзакцией, для других параллельно выполняющихся транзакций. Это важно для управления конкурентным доступом к данным и предотвращения различных аномалий при одновременном выполнении транзакций. В SQL стандарте и большинстве реляционных СУБД определены четыре уровня изоляции транзакций:

Уровень изоляции:    | Грязное чтение | Неповторяющееся чтение | Фантомное чтение
---------------------------------------------------------------------------------
READ UNCOMMITTED     |       +        |            +           |        +
READ COMMITTED       |       -        |            +           |        +
REPEATABLE READ      |       -        |            -           |        +
SERIALIZABLE         |       -        |            -           |        -

Read Uncommitted (Чтение неподтвержденных данных):
Описание: Этот уровень изоляции позволяет транзакции читать данные, которые были изменены, но еще не подтверждены (не зафиксированы) другими транзакциями.

Аномалии:
Грязное чтение (Dirty Read): Чтение данных, которые могут быть откатаны в другой транзакции.
Неповторяющееся чтение (Non-repeatable Read): Чтение данных, которые могут быть изменены другой транзакцией между двумя чтениями.
Фантомное чтение (Phantom Read): Чтение строк, которые могут быть добавлены или удалены другой транзакцией.

Read Committed (Чтение подтвержденных данных):
Описание: Транзакция может читать только те данные, которые были подтверждены (зафиксированы) другими транзакциями. Это предотвращает грязное чтение.

Аномалии:
Неповторяющееся чтение (Non-repeatable Read): Чтение данных, которые могут быть изменены другой транзакцией между двумя чтениями.
Фантомное чтение (Phantom Read): Чтение строк, которые могут быть добавлены или удалены другой транзакцией.

Repeatable Read (Повторяющееся чтение):
Описание: Этот уровень изоляции гарантирует, что данные, прочитанные в начале транзакции, не будут изменены другими транзакциями до завершения текущей транзакции. Это предотвращает как грязное чтение, так и неповторяющееся чтение.

Аномалии:
Фантомное чтение (Phantom Read): Чтение строк, которые могут быть добавлены или удалены другой транзакцией.

Serializable (Сериализуемость):
Описание: Самый высокий уровень изоляции, при котором транзакции выполняются так, как если бы они выполнялись последовательно, одна за другой, а не параллельно. Это предотвращает все виды аномалий.

Аномалии:
Не допускаются. Гарантирует отсутствие грязного чтения, неповторяющегося чтения и фантомного чтения.

Пояснение аномалий:

Грязное чтение (Dirty Read):
Одна транзакция читает данные, измененные другой транзакцией, которая еще не была зафиксирована. Если вторая транзакция откатывается, то первая транзакция использует некорректные данные.

Неповторяющееся чтение (Non-repeatable Read):
Данные, которые были прочитаны одной транзакцией, могут изменяться другими транзакциями, что приводит к тому, что повторное чтение тех же данных возвращает разные результаты.

Фантомное чтение (Phantom Read):
Одна транзакция читает набор строк, которые соответствуют определенному условию. Если другая транзакция добавляет или удаляет строки, соответствующие этому условию, то повторное выполнение запроса в первой транзакции возвращает другой набор строк.

Примеры использования уровней изоляции в Spring:
В Spring уровень изоляции транзакций можно задать с помощью аннотации @Transactional, есть и другие способы

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() {
    }
}

propagation (тип: Propagation):
Определяет, как транзакция должна распространяться на методы, вызываемые внутри текущей транзакции.

Значения:

Propagation.REQUIRED (по умолчанию):
Использует текущую транзакцию или создает новую, если текущая отсутствует.

Propagation.REQUIRES_NEW:
Всегда создает новую транзакцию, приостанавливая текущую, если она существует.

Propagation.SUPPORTS:
Использует текущую транзакцию, если она существует, в противном случае выполняется без транзакции.

Propagation.NOT_SUPPORTED:
Выполняет метод вне транзакции, приостанавливая текущую транзакцию, если она существует.

Propagation.MANDATORY:
Требует существования текущей транзакции, выбрасывает исключение, если ее нет.

Propagation.NEVER:
Выполняется без транзакции, выбрасывает исключение, если текущая транзакция существует.

Propagation.NESTED:
Создает вложенную транзакцию, если существует текущая транзакция.

@Transactional(propagation = Propagation.REQUIRED)
fun myTransactionalMethod() {

isolation (тип: Isolation):
Определяет уровень изоляции транзакции.

Значения:

Isolation.DEFAULT (по умолчанию):
Использует уровень изоляции базы данных.

Isolation.READ_UNCOMMITTED:
Самый низкий уровень изоляции, позволяет “грязное чтение”.

Isolation.READ_COMMITTED:
Гарантирует, что данные, считанные в транзакции, уже зафиксированы.

Isolation.REPEATABLE_READ:
Гарантирует, что данные, считанные в транзакции, не изменятся до ее завершения.

Isolation.SERIALIZABLE:
Самый высокий уровень изоляции, гарантирует полную изоляцию транзакций.

@Transactional(isolation = Isolation.REPEATABLE_READ)
fun myTransactionalMethod() {
    // Код будет выполнен в рамках транзакции с уровнем изоляции REPEATABLE_READ
}

timeout (тип: Int):
Определяет максимальное время (в секундах), которое транзакция может выполняться до принудительного отката.
Значение по умолчанию -1 означает отсутствие ограничения по времени.
Если транзакция не завершится за указанное время, будет выброшено исключение TransactionTimedOutException.

@Transactional(timeout = 30)
fun myTransactionalMethod() {
    // Код должен быть выполнен в течение 30 секунд
}

readOnly (тип: Boolean):
Указывает, что транзакция предназначена только для чтения. Значение по умолчанию false.
Установка в true позволяет оптимизировать производительность, так как транзакция не будет содержать изменений.

@Transactional(readOnly = true)
fun myReadOnlyMethod() {
    // Код выполняется в режиме только для чтения
}

rollbackFor (тип: Array<Class<out Throwable>>):
Указывает массив исключений, при возникновении которых должна произойти откат транзакции.

@Transactional(rollbackFor = [RuntimeException::class, IOException::class])
fun myMethod() {
    // Транзакция будет откатана, если будут выброшены RuntimeException или IOException
}

rollbackForClassName (тип: Array<String>):
Указывает массив имен исключений (в виде строк), при возникновении которых должна произойти откат транзакции.

noRollbackFor (тип: Array<Class<out Throwable>>):
Указывает массив исключений, при возникновении которых транзакция не должна откатываться.

@Transactional(noRollbackFor = [CustomException::class])
fun myMethod() {
    // Транзакция не будет откатана, если будет выброшен CustomException
}

noRollbackForClassName (тип: Array<String>):
Указывает массив имен исключений (в виде строк), при возникновении которых транзакция не должна откатываться.

transactionManager:
Указывает, какой транзакционный менеджер использовать, если их несколько

@Transactional(transactionManager = "customTransactionManager")
fun myMethod() {
    // Метод будет использовать customTransactionManager
}

value:
Синоним для transactionManager. Можно использовать для указания транзакционного менеджера.

@Transactional("customTransactionManager")
fun myMethod() {
    // Метод будет использовать customTransactionManager
}

Пример использования всех параметров:

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() {
    }
}

Механизм работы @Transactional в Spring основан на использовании прокси и аспектно-ориентированного программирования для управления транзакциями. Прокси перехватывает вызовы методов и взаимодействует с транзакционным менеджером для начала, подтверждения или отката транзакций. Это позволяет автоматически обрабатывать транзакции, упрощая разработку надежных и согласованных приложений.

Для мониторинга транзакций в логах в Spring приложениях, необходимо настроить логирование соответствующих компонентов Spring и Hibernate (если используется JPA/Hibernate). Основное внимание следует уделить началу, коммиту, откату транзакций и потенциальным ошибкам.

logging:
  level:
    org:
      springframework:
        transaction: DEBUG
      hibernate:
        SQL: DEBUG
        transaction: DEBUG
        type:
          descriptor:
            sql:
              BasicBinder: TRACE

Пример конфигурации логирования для 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>

Что искать в логах:

Начало транзакции:
Сообщения, указывающие на начало новой транзакции.
Например: “Creating new transaction with name […]”

Коммит транзакции:
Сообщения о фиксации транзакции.
Например: “Initiating transaction commit for […]”

Откат транзакции:
Сообщения о выполнении отката транзакции.
Например: “Initiating transaction rollback for […]”

Ошибки и исключения:
Сообщения об ошибках и исключениях, связанных с транзакциями.
Например: “Rolling back transaction because of exception […]”

Инструменты для мониторинга:

Spring Boot Actuator:
Позволяет мониторить состояние вашего приложения, включая информацию о транзакциях.

Application Performance Monitoring (APM) Tools:
Инструменты такие как New Relic, Dynatrace, или AppDynamics предоставляют детальную информацию о транзакциях, запросах, производительности и ошибках.

Redis (Remote Dictionary Server) — это высокопроизводительный, in-memory key-value хранилище, которое поддерживает множество структур данных, таких как строки, списки, множества, хеши и сортированные множества. Redis часто используется для кэширования данных, управления сессиями, очередей сообщений, чатов, счетчиков и других сценариев, требующих высокой производительности и низкой задержки.

Интеграция Spring Boot с Redis может быть выполнена с использованием Spring Data Redis, который предоставляет удобный и мощный API для работы с Redis. Вот пошаговый пример, как настроить и использовать Redis в Spring Boot приложении.

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)
    }
    
}

В Spring-приложении вы можете использовать подходы для мониторинга изменений в базе данных и выполнения действий, таких как отправка запроса, при изменении значения в базе данных. Один из популярных способов достижения этого — использование триггеров в базе данных вместе с Spring Events или, если используется NoSQL база данных, таких как MongoDB, использование его функциональности Change Streams.

Пример для реляционной базы данных (например, PostgreSQL) с использованием триггеров и слушателей:

Создание триггера в базе данных:
Создайте триггер в базе данных для отслеживания изменений в таблице.

Пример для PostgreSQL:
Создайте функцию триггера

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 TRIGGER table_update_trigger
AFTER UPDATE ON your_table
FOR EACH ROW
EXECUTE FUNCTION notify_table_update();

Настройка слушателя в Spring Boot:
Используйте JDBC для прослушивания уведомлений от базы данных.

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}")
                                // Отправьте запрос при получении уведомления
                                sendRequest(notification.parameter)
                            }
                        }
                        Thread.sleep(1000) // Установите задержку между проверками
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }.start()
    }
    
    private fun sendRequest(id: String) {
        // Логика отправки запроса
        println("Sending request for id: $id")
        // Например, можно использовать RestTemplate или WebClient для отправки HTTP-запроса
    }

}

Пример для MongoDB с использованием Change Streams:
Если вы используете MongoDB, вы можете использовать Change Streams для отслеживания изменений.

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}")
                        // Отправьте запрос при получении изменения
                        sendRequest(change.fullDocument)
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }.start()
    }

    private fun sendRequest(document: Document?) {
        // Логика отправки запроса
        println("Sending request for document: $document")
        // Например, можно использовать RestTemplate или WebClient для отправки HTTP-запроса
    }

}
Repository
    ├── CrudRepository
    │     ├── PagingAndSortingRepository
    │     │     ├── JpaRepository
    │     │     └── MongoRepository
    │     └── ReactiveCrudRepository
    │           └── R2dbcRepository

CrudRepository:
предоставляет основные методы CRUD (создание, чтение, обновление, удаление) для работы с базой данных. Это базовый интерфейс для всех репозиториев в Spring Data.

import org.springframework.data.repository.CrudRepository


interface PersonRepository : CrudRepository<Person, Long>

JpaRepository:
расширяет CrudRepository и PagingAndSortingRepository, предоставляя дополнительные методы для работы с JPA (Java Persistence API), такие как методы для пакетных операций и методы для работы с флашированием.

import org.springframework.data.jpa.repository.JpaRepository


interface PersonRepository : JpaRepository<Person, Long>

PagingAndSortingRepository:
расширяет CrudRepository и предоставляет методы для выполнения операций с сортировкой и пагинацией (разбиение на страницы).

import org.springframework.data.repository.PagingAndSortingRepository


interface PersonRepository : PagingAndSortingRepository<Person, Long>

MongoRepository:
используется для работы с MongoDB. Он расширяет PagingAndSortingRepository и предоставляет методы для работы с MongoDB.

import org.springframework.data.mongodb.repository.MongoRepository


interface PersonRepository : MongoRepository<Person, String>

ReactiveCrudRepository:
используется для реактивного программирования с поддержкой Project Reactor. Он предоставляет асинхронные методы для CRUD операций.

import org.springframework.data.repository.reactive.ReactiveCrudRepository
import reactor.core.publisher.Mono


interface PersonRepository : ReactiveCrudRepository<Person, Long> {
  
    fun findByFirstName(firstName: String): Mono<Person>
    
}

R2dbcRepository:
используется для работы с базами данных с использованием 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>
    
}

Jenkins — это популярный инструмент для непрерывной интеграции (Continuous Integration, CI) и непрерывного развертывания (Continuous Deployment, CD). Он используется для автоматизации различных этапов разработки программного обеспечения, таких как сборка, тестирование и развертывание приложений. Jenkins — это проект с открытым исходным кодом, который поддерживает множество плагинов для интеграции с различными инструментами и сервисами.

Основные возможности Jenkins:

Автоматизация процессов:
Jenkins позволяет автоматизировать сборку, тестирование и развертывание приложений. Это помогает уменьшить ручной труд и снизить вероятность ошибок.

Плагины:
Jenkins поддерживает более 1500 плагинов, которые позволяют интегрировать его с множеством инструментов и сервисов, таких как Git, Maven, Gradle, Docker, Kubernetes и многими другими.

Поддержка различных языков и технологий:
Jenkins поддерживает множество языков программирования и технологий, что делает его универсальным инструментом для CI/CD.

Масштабируемость:
Jenkins можно настроить для работы в распределенной среде с использованием агентов (nodes) для выполнения задач на различных машинах.

Интеграция с системами контроля версий:
Jenkins может интегрироваться с различными системами контроля версий, такими как Git, Subversion, Mercurial и другими.

Веб-интерфейс и API:
Jenkins предоставляет удобный веб-интерфейс для управления и мониторинга задач, а также RESTful API для интеграции с другими системами.

Пример использования Jenkins для Spring Boot проекта:

Установка Jenkins:
Вы можете установить Jenkins с помощью Docker, скачав дистрибутив с официального сайта или используя пакетный менеджер вашей операционной системы.

docker run -p 8080:8080 -p 50000:50000 jenkins/jenkins:lts

Настройка Jenkins:
Откройте браузер и перейдите на http://localhost:8080.
Введите пароль администратора, который можно найти в файле /var/jenkins_home/secrets/initialAdminPassword внутри контейнера.
Следуйте инструкциям мастера установки для завершения настройки Jenkins.
Шаг 2: Настройка Jenkins для сборки Spring Boot приложения

Установка плагинов:
В Jenkins перейдите в Manage Jenkins -> Manage Plugins.
Установите следующие плагины:
Gradle Plugin
Git Plugin

Настройка Gradle:
Убедитесь, что Gradle установлен на вашем Jenkins-сервере. Если нет, установите его.
В Jenkins перейдите в Manage Jenkins -> Global Tool Configuration.
В разделе Gradle добавьте новую установку Gradle, указав имя и путь к Gradle (или выберите автоматическую установку).

Создание нового задания (Job):
В Jenkins перейдите на главную страницу и нажмите New Item.
Введите имя проекта и выберите Freestyle project.
Нажмите OK.

Настройка источника кода:
В разделе Source Code Management выберите Git.
Укажите URL репозитория вашего проекта и учетные данные для доступа.

Настройка сборки:
В разделе Build нажмите Add build step и выберите Invoke Gradle script.
В поле Switches введите clean build.
В поле Tasks введите build.

Настройка триггеров сборки:
В разделе Build Triggers выберите способ, как Jenkins будет запускать сборку:
Poll SCM: Для опроса репозитория на изменения.
Build periodically: Для запуска сборок по расписанию.
GitHub hook trigger for GITScm polling: Для автоматического запуска сборки при пуше в репозиторий.

Настройка пост-сборочных действий:
Вы можете добавить пост-сборочные действия, такие как отправка уведомлений или развертывание приложения.

Nexus — это репозиторий менеджер, используемый для хранения, управления и распространения артефактов, таких как библиотеки, пакеты и другие компоненты, используемые в процессе разработки программного обеспечения. Он поддерживает множество форматов, включая Maven, npm, NuGet, Docker, и многие другие.

Основные возможности Nexus:

Хранение артефактов:
Nexus предоставляет централизованное место для хранения всех артефактов, используемых в процессе разработки, таких как библиотеки, плагины и другие зависимости.

Управление зависимостями:
Nexus помогает управлять зависимостями проектов, обеспечивая доступ к необходимым артефактам и их версиям.

Кэширование удаленных репозиториев:
Nexus может кэшировать артефакты из удаленных репозиториев, что помогает уменьшить задержки и повысить производительность.

Поддержка множества форматов:
Nexus поддерживает различные форматы репозиториев, такие как Maven, npm, NuGet, Docker и другие.

Управление доступом:
Nexus предоставляет гибкую систему управления доступом, позволяя контролировать, кто может загружать и скачивать артефакты.

Интеграция с CI/CD:
Nexus легко интегрируется с инструментами для непрерывной интеграции и развертывания (CI/CD), такими как Jenkins, для автоматизации процессов сборки и развертывания.

Вы можете установить Nexus с использованием Docker:

docker run -d -p 8081:8081 --name nexus sonatype/nexus3

После запуска контейнера Nexus, откройте браузер и перейдите на http://localhost:8081. Используйте следующие данные для входа по умолчанию:
Логин: admin
Пароль: вы можете найти пароль в файле admin.password, расположенном в директории /nexus-data/admin.password внутри контейнера.

План запроса (Query Plan) — это набор инструкций, созданных системой управления базами данных (СУБД), которые описывают, как СУБД выполнит SQL-запрос. План запроса содержит информацию о порядке выполнения операций, используемых индексов, методах доступа к данным и других аспектах, которые влияют на производительность запроса.

Основные компоненты плана запроса:

Последовательность операций:
Операции могут включать сканирование таблицы, использование индексов, соединения таблиц, сортировку и агрегирование данных.

Используемые индексы:
План запроса указывает, какие индексы используются для ускорения доступа к данным.

Методы доступа к данным:
Методы могут включать полное сканирование таблицы, индексированное сканирование, поиск по первичному ключу и т. д.

Порядок соединения таблиц:
План запроса описывает порядок, в котором таблицы соединяются, и методы соединения (например, вложенные циклы, хеш-соединение, слияние).

В PostgreSQL можно использовать команду EXPLAIN, чтобы получить план запроса:

EXPLAIN SELECT * FROM users WHERE id = 1;

Пример плана запроса:

Seq Scan on users
(cost=0.00..35.50 rows=1 width=64)
Filter: (id = 1)

Seq Scan:
Указывает на последовательное сканирование таблицы users.

cost=0.00..35.50:
Оценка стоимости выполнения запроса. Первое значение (0.00) — это стоимость старта, а второе (35.50) — полная стоимость выполнения запроса.

rows=1:
Ожидаемое количество строк, возвращаемых запросом.

width=64:
Ожидаемый средний размер строки в байтах.

Filter:
Указывает на условие фильтрации (id = 1).

Оптимизация запросов с использованием плана запроса:
Анализ плана запроса позволяет выявить узкие места и принять меры для оптимизации запросов. Вот несколько примеров оптимизаций:

Использование индексов:
Создание индексов на столбцах, используемых в условиях WHERE, JOIN и ORDER BY.

Избегание полного сканирования таблицы:
Использование индексированных сканирований вместо полного сканирования таблицы для улучшения производительности.

Оптимизация соединений таблиц:
Выбор подходящего метода соединения таблиц (например, хеш-соединение, слияние) в зависимости от объема данных.\

Рефакторинг запросов:
Переписывание сложных запросов, чтобы уменьшить количество операций и улучшить производительность.
Пример использования индекса

Создание индекса:

CREATE INDEX idx_users_id ON users (id);

Получение плана запроса после создания индекса:

EXPLAIN SELECT * FROM users WHERE id = 1;

Пример плана запроса после создания индекса:

Index Scan using idx_users_id on users
(cost=0.28..8.30 rows=1 width=64)
Index Cond: (id = 1)

Index Scan:
Указывает на использование индексированного сканирования по индексу idx_users_id.

cost=0.28..8.30:
Оценка стоимости выполнения запроса, которая значительно уменьшилась по сравнению с последовательным сканированием таблицы.

Index Cond:
Указывает на условие использования индекса (id = 1).

Пример создания индекса в 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

Используйте аннотацию @Table и @Index для создания индексов на уровне сущности.

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
  
)

Пример SQL-запроса для проверки индекса:

SHOW INDEX FROM users;

Аномалии в работе с базой данных обычно возникают в контексте операций с транзакциями и параллельного доступа к данным. Эти аномалии могут привести к непредсказуемому поведению и некорректным результатам.

Основные типы аномалий включают:
Dirty Read
Non-Repeatable Read
Phantom Read
Lost Update
Double Update Anomaly

Грязное чтение (Dirty Read):
Возникает, когда одна транзакция читает данные, которые были изменены другой транзакцией, но ещё не зафиксированы (не завершены). Если вторая транзакция откатывается, первая транзакция содержит неверные данные.

Пример:
Транзакция A обновляет значение столбца, но не фиксирует изменения.
Транзакция B читает обновленное значение.
Транзакция A откатывается.
Транзакция B остается с некорректными данными.

Неповторяемое чтение (Non-repeatable Read):
Случается, когда одна транзакция дважды читает одну и ту же запись, а другая транзакция изменяет или удаляет эту запись между этими двумя чтениями. В результате, первое и второе чтение одной и той же записи возвращают разные результаты.

Пример:
Транзакция A читает значение столбца.
Транзакция B обновляет значение столбца и фиксирует изменения.
Транзакция A снова читает значение столбца и видит другое значение.

Фантомное чтение (Phantom Read):
Происходит, когда одна транзакция дважды выполняет одно и то же запрос для выбора набора строк, а другая транзакция между этими запросами добавляет или удаляет строки, соответствующие критериям запроса. В результате, первый и второй запросы возвращают разные наборы строк.

Пример:
Транзакция A выполняет запрос, возвращающий набор строк.
Транзакция B вставляет новую строку, которая соответствует условиям запроса транзакции A, и фиксирует изменения.
Транзакция A снова выполняет тот же запрос и видит дополнительную строку.

Потерянное обновление (Lost Update):
Происходит, когда две транзакции одновременно читают одну и ту же запись и затем обновляют её. Последнее обновление перезаписывает предыдущее, и изменения первой транзакции теряются.

Пример:
Транзакция A читает значение столбца.
Транзакция B читает то же значение столбца.
Транзакция A обновляет значение и фиксирует изменения.
Транзакция B обновляет значение и фиксирует изменения, перезаписывая изменения, сделанные транзакцией A.

Двойное обновление (Double Update Anomaly):
Эта аномалия происходит, когда одно и то же обновление применяется дважды к одной и той же строке из-за повторного выполнения транзакции.

Пример:
Транзакция A увеличивает значение столбца на 1.
Транзакция A завершается с ошибкой и автоматически повторяется.
Значение столбца увеличивается на 2 вместо 1.

Необоснованное зависание (Uncommitted Dependency):
Возникает, когда одна транзакция зависает из-за того, что ожидает завершения другой транзакции, которая держит блокировку на необходимом ресурсе. Это может привести к снижению производительности и ухудшению отклика системы.

Перекрёстные обновления (Write Skew):
Происходит, когда две транзакции одновременно изменяют различные, но связанные между собой данные, что приводит к неконсистентному состоянию системы. Например, если две транзакции проверяют условия и на их основе обновляют данные, возможно нарушение согласованности, если условия проверяются до обновления данных.

Управление аномалиями с помощью уровней изоляции:
Различные уровни изоляции транзакций определены стандартом SQL для управления этими аномалиями

Isolation.READ UNCOMMITTED:
Наименьший уровень изоляции. Позволяет грязные чтения, неповторяющиеся чтения и фантомные чтения.

Isolation.READ COMMITTED:
Запрещает грязные чтения. Позволяет неповторяющиеся чтения и фантомные чтения.

Isolation.REPEATABLE READ:
Запрещает грязные чтения и неповторяющиеся чтения. Позволяет фантомные чтения.

Isolation.SERIALIZABLE:
Наивысший уровень изоляции. Запрещает грязные чтения, неповторяющиеся чтения и фантомные чтения.

Пример настройки уровня изоляции в 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() {
    }

}

По модальности блокировки:

Shared Locks (Разделяемые блокировки):
позволяет нескольким транзакциям одновременно читать данные. Однако ни одна из этих транзакций не может изменять данные, пока держится разделяемая блокировка.

Пример:
Транзакция A берет Shared Lock на строку.
Транзакция B может также взять Shared Lock на ту же строку и читать данные.
Транзакция C не может взять Exclusive Lock на эту строку, пока держатся Shared Locks транзакций A и B.

-- Транзакция A берет разделяемую блокировку
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 (Исключительные блокировки):
позволяет транзакции как читать, так и изменять данные. Только одна транзакция может иметь исключительную блокировку на ресурсе, и никакая другая транзакция не может взять разделяемую или исключительную блокировку на этот ресурс, пока держится исключительная блокировка.

Пример:
Транзакция A берет Exclusive Lock на строку.
Транзакция B не может взять Shared Lock или Exclusive Lock на эту строку, пока держится Exclusive Lock транзакции A.

-- Транзакция B берет исключительную блокировку
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?

}

Пример использования блокировок в транзакциях Spring:

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)
        // Ваш код здесь
    }

    @Transactional
    fun performExclusiveLockOperation(id: Long) {
        val entity = myRepository.findWithExclusiveLock(id)
        // Ваш код здесь
    }

    @Transactional
    fun performExclusiveLockOperationWithEntityManager(id: Long) {
        val entity = entityManager.find(MyEntity::class.java, id, LockModeType.PESSIMISTIC_WRITE)
        // Ваш код здесь
    }

}

По уровню затрагиваемых данных:

Блокировка строки (Row Lock):
Блокирует отдельную строку в таблице. Это позволяет нескольким пользователям работать с разными строками одной и той же таблицы одновременно.

Блокировка страницы (Page Lock):
Блокирует страницу данных, содержащую несколько строк. Эффективна при массовых операциях с данными, но может приводить к большему количеству конфликтов по сравнению с блокировкой строк.

Блокировка таблицы (Table Lock):
Блокирует всю таблицу. Это предотвращает любые другие операции с этой таблицей, пока блокировка активна. Используется, когда нужно выполнить операции, затрагивающие большую часть или всю таблицу.

По способу управления:

Оптимистическая блокировка:
Подход, при котором система предполагает, что конфликты маловероятны, и не устанавливает блокировку до момента фиксации изменений. Вместо этого при попытке сохранения изменений проверяется, не были ли изменены данные с момента их последнего чтения.

Пессимистическая блокировка:
Подход, при котором система предполагает, что конфликты вероятны, и устанавливает блокировки на данные во время чтения, удерживая их до окончания транзакции. Это минимизирует риск конфликтов, но может привести к уменьшению производительности и возникновению взаимных блокировок.

Также есть:

Intent Lock:
Используется для указания намерений по установке блокировок на более низких уровнях. Например, если транзакция собирается блокировать строки внутри страницы, она сначала установит блокировку намерений на уровне страницы.

Deadlock:
Взаимная блокировка возникает, когда две или более транзакций ожидают, пока другая освободит ресурс, который им нужен. СУБД обычно обнаруживают взаимные блокировки и автоматически прерывают одну из транзакций для разрешения конфликта.

В Spring Framework управление транзакциями может быть реализовано несколькими способами. Основные способы включают декларативное и программное управление транзакциями.

Декларативное управление транзакциями:

@Transactional:
Используется для указания методов или классов, которые должны выполняться в рамках транзакции.
Можно настроить параметры, такие как уровень изоляции, распространение, тайм-аут и т.д.

import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional


@Service
class MyService {

    @Transactional
    fun executeTransactionalMethod() {
        // Здесь код будет выполнен в рамках транзакции
    }
    
}

XML-конфигурация:
Возможна конфигурация транзакционного менеджера и аспектов в XML.

<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>

Программное управление транзакциями:
Программное управление транзакциями подразумевает явное использование PlatformTransactionManager для управления транзакциями в коде.

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 {
            // Выполнение бизнес-логики
            // Например, вставка данных в базу данных
            transactionManager.commit(transactionStatus)
        } catch (ex: Exception) {
            transactionManager.rollback(transactionStatus)
            throw ex
        }
    }
    
}

Способы управления транзакциями в зависимости от контекста:

JDBC-транзакции:
Используется DataSourceTransactionManager.
Подходит для управления транзакциями на уровне JDBC.

@Bean
fun transactionManager(dataSource: DataSource): PlatformTransactionManager {
    return DataSourceTransactionManager(dataSource)
}

JPA-транзакции:
Используется JpaTransactionManager.
Подходит для управления транзакциями на уровне JPA.

@Bean
fun transactionManager(entityManagerFactory: EntityManagerFactory): PlatformTransactionManager {
    return JpaTransactionManager(entityManagerFactory)
}

Hibernate-транзакции:
Используется HibernateTransactionManager.
Подходит для управления транзакциями на уровне Hibernate.

@Bean
fun transactionManager(sessionFactory: SessionFactory): PlatformTransactionManager {
    return HibernateTransactionManager(sessionFactory)
}

JTA-транзакции:
Используется JtaTransactionManager.
Подходит для управления распределенными транзакциями в корпоративных приложениях.

@Bean
fun transactionManager(): PlatformTransactionManager {
    return JtaTransactionManager()
}

TransactionOperations и TransactionTemplate:
Обеспечивает программное управление транзакциями с упрощенным синтаксисом.
TransactionOperations — это интерфейс в Spring, который определяет основные операции для работы с транзакциями. TransactionTemplate — это конкретная реализация этого интерфейса.

Основные методы TransactionOperations:

<T> execute(TransactionCallback<T> action):
Выполняет транзакцию и возвращает результат.

<T> executeWithoutResult(TransactionCallbackWithoutResult action):
Выполняет транзакцию без возвращаемого результата.

TransactionStatus:
предоставляет методы для управления текущим состоянием транзакции. Этот объект передается в метод транзакционного колбэка и может быть использован для контроля над ходом транзакции, включая установку точки сохранения (savepoint), откат транзакции и проверку её состояния.

Основные методы TransactionStatus:

setRollbackOnly():
Помечает текущую транзакцию для отката. После вызова этого метода транзакция будет откатана при завершении, даже если метод завершится успешно.

isNewTransaction():
Возвращает true, если текущий статус транзакции представляет собой новую транзакцию, а не существующую.

hasSavepoint():
Возвращает true, если в текущей транзакции установлен какой-либо savepoint.

createSavepoint():
Создает новый savepoint в текущей транзакции. Savepoint позволяет откатить часть транзакции без отката всей транзакции.

releaseSavepoint(Object savepoint):
Освобождает (удаляет) указанный savepoint.

rollbackToSavepoint(Object savepoint):
Откатывает транзакцию до указанного 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")
                // Если нужно, можно вызвать status.setRollbackOnly() для отката транзакции
                status.rollbackToSavepoint(savepoint)
            }
        })
    }

    fun executeInTransactionWithResult(): String {
        return transactionTemplate.execute(TransactionCallback { status ->
            println("Executing in transaction with result")
            "Transaction Result"
        }) ?: "Default Result"
    }
}

TransactionSynchronizationManager:
Предоставляет низкоуровневый доступ для управления синхронизацией транзакций.

import org.springframework.transaction.support.TransactionSynchronizationManager


class MyService {

    fun someMethod() {
        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            // Текущая транзакция активна
        }
    }
    
}

Для работы с базой данных в реактивном стиле в WebFlux вы можете использовать проект Spring Data R2DBC (Reactive Relational Database Connectivity). Этот проект позволяет работать с реляционными базами данных в реактивном стиле.

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)
    
}

Асинхронные библиотеки:
Убедитесь, что используете асинхронные драйверы для вашей базы данных. В случае PostgreSQL это r2dbc-postgresql.

Обработка ошибок:
Важно учитывать обработку ошибок и управление транзакциями в реактивных приложениях.

Ппопулярный инструмент для управления версионностью базы данных. Он позволяет организовать миграции базы данных, то есть последовательность изменений (например, создание таблиц, изменение схемы, наполнение данными и т.д.), и автоматизировать их применение.

Основные возможности Flyway:

Версионность миграций:
Каждая миграция идентифицируется уникальным номером версии, что позволяет отслеживать изменения в базе данных и гарантировать, что они применяются в правильном порядке.

Поддержка различных баз данных:
Flyway поддерживает множество баз данных, таких как PostgreSQL, MySQL, Oracle, SQL Server и многие другие.

Интеграция с различными инструментами:
Flyway легко интегрируется с популярными сборщиками проектов и фреймворками, такими как Maven, Gradle, Spring Boot и др.

Автоматическое обнаружение и выполнение миграций:
Flyway автоматически обнаруживает новые миграции и применяет их к базе данных.

Поддержка различных типов миграций:
Flyway поддерживает SQL и Java миграции, что позволяет гибко управлять изменениями в базе данных.

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

где

logging:
  level:
    org.springframework.data.r2dbc: DEBUG

устанавливает уровень логирования для пакета org.springframework.data.r2dbc на уровень DEBUG. Это значит, что будут выводиться детализированные сообщения о выполнении операций с базой данных через R2DBC.

spring:
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/database
    username: user
    password: 123

Параметры конфигурируют подключение к базе данных PostgreSQL с использованием R2DBC (Reactive Relational Database Connectivity):

spring.r2dbc.url:
URL для подключения к базе данных через R2DBC.

spring.r2dbc.username:
Имя пользователя для подключения к базе данных.

spring.r2dbc.password:
Пароль для подключения к базе данных.

flyway:
    url: jdbc:postgresql://localhost:5432/database
    user: user
    password: 123
    locations: classpath:db/migration

Параметры конфигурируют Flyway для управления миграциями базы данных:

spring.flyway.url:
URL для подключения к базе данных через JDBC.

spring.flyway.user:
Имя пользователя для подключения к базе данных.

spring.flyway.password:
Пароль для подключения к базе данных.

Важно отметить, что Flyway использует JDBC для выполнения миграций, даже если само приложение использует R2DBC для работы с базой данных.

jwt:
  secret: very-very-secret-key-should-be-almost-infinity
  expiration: 86400

Параметры конфигурируют JWT для аутентификации и авторизации:

jwt.secret:
Секретный ключ для подписи JWT. Этот ключ должен быть надежным и секретным, чтобы обеспечить безопасность токенов.

jwt.expiration:
Время жизни токена в секундах. В данном случае, токен будет действителен в течение 86400 секунд (24 часа).

Миграции должны располагаться в директории src/main/resources/db/migration и следовать определенному формату именования: V__.sql

Пример миграции V1__Create_user_table.sql:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE
);

Flyway автоматически выполнит миграции при запуске приложения Spring Boot. Если в базе данных будут обнаружены новые миграции, они будут применены.

В Spring приложениях приватные ключи и другие конфиденциальные данные должны храниться безопасно, чтобы предотвратить несанкционированный доступ. Вот несколько рекомендаций по их хранению:

Spring Boot Configuration Properties:
Используйте файл application.properties или application.yml, но не храните в них сами ключи, а лишь указывайте пути к ним.

Environment Variables:
Окружающие переменные (environment variables) являются безопасным способом хранения конфиденциальной информации.

@Value("\${my.secret.key}")
private lateinit var mySecretKey: String

External Configuration Server:
Используйте Spring Cloud Config Server, который позволяет централизованно управлять конфигурацией и безопасно хранить ключи. Данные могут быть зашифрованы и защищены.

Vault:
HashiCorp Vault — это мощное решение для хранения секретов. Spring Cloud Vault позволяет интегрировать Vault с вашим приложением.

spring:
  cloud:
    vault:
      uri: http://127.0.0.1:8200
      token: s.xxxxxxxx
      kv:
        enabled: true
        backend: secret
        default-context: application

Java Keystore (JKS):
Хранение ключей в JKS является хорошей практикой. Пример загрузки ключа из хранилища JKS:

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:
Если ваше приложение разворачивается в облаке, используйте облачные сервисы для хранения секретов.

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
}

Шифрование данных:
Независимо от места хранения, данные должны быть зашифрованы. Используйте библиотеки, такие как Jasypt, для шифрования данных.

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
    }
    
}

Конфигурация кеша:
Включение кеширования в конфигурационном классе с помощью аннотации @EnableCaching.
Настройка кеша в ресурсных файлах (например, application.yml).

Использование аннотаций кеширования:
Аннотации, такие как @Cacheable, @CachePut, @CacheEvict, и @Caching, указывают, какие методы должны быть кешированы, обновлены или очищены в кеш-памяти.

Обработка аннотаций:
Spring обрабатывает эти аннотации с помощью прокси (Proxy) или аспектов (Aspects), добавляя дополнительный код для работы с кешем перед вызовом методов.

Работа с кешем:
При первом вызове метода, помеченного аннотацией @Cacheable, выполняется исходный метод и результат сохраняется в кеш.
При последующих вызовах того же метода с теми же параметрами результат извлекается из кеша, минуя выполнение исходного метода.

Обновление и удаление данных в кеше:
Аннотация @CachePut обновляет данные в кеше после выполнения метода.
Аннотация @CacheEvict удаляет данные из кеша, чтобы гарантировать, что устаревшие данные не будут возвращены.

Пример работы кеша:
Рассмотрим работу кеширования на примере метода getUserById

Первый вызов метода getUserById(1):
Метод выполняется.
Результат (User(id=1, name=”User1″)) сохраняется в кеш с ключом

Последующий вызов метода getUserById(1):
Spring проверяет наличие результата в кеше.
Если результат есть, он возвращается из кеша, и метод не выполняется повторно.

Обновление данных с помощью updateUser(User(1, “UpdatedUser1”)):
Метод выполняется.
Кеш обновляется новым значением (User(id=1, name=”UpdatedUser1″)) с ключом 1.

Удаление данных из кеша с помощью deleteUser(1):
Метод выполняется.
Запись с ключом 1 удаляется из кеша.

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:
Кеширует результат метода.

@CachePut:
Обновляет кеш после выполнения метода.

@CacheEvict:
Удаляет записи из кеша.

@Service
class UserService {

    @Cacheable("users", key = "#id")
    fun getUserById(id: Long): User {
        // Симуляция долгого запроса
        Thread.sleep(3000)
        return User(id, "User$id")
    }

    @CachePut(value = ["users"], key = "#user.id")
    fun updateUser(user: User): User {
        // Логика обновления пользователя
        return user
    }

    @CacheEvict(value = ["users"], key = "#id")
    fun deleteUser(id: Long) {
        // Логика удаления пользователя
    }

    @Caching(
        put = [CachePut(value = ["users"], key = "#user.id")],
        evict = [CacheEvict(value = ["users_list"], allEntries = true)]
    )
    fun saveUser(user: User): User {
        // Логика сохранения пользователя
        return user
    }
}

Возможные конфигурации кеша:

Simple (ConcurrentMapCache):
Это тип кеша по умолчанию, который использует ConcurrentHashMap для хранения кеша в памяти.

JCache (JSR-107):
Это стандарт Java для кеширования, который позволяет интегрироваться с различными провайдерами кеша, такими как Ehcache, Hazelcast, Infinispan и другими.

Ehcache:
Популярный и мощный кеш-провайдер, поддерживающий расширенные функции управления кешем.

Hazelcast:
Кластерный распределенный кеш, который также может быть использован в качестве сетевого игрового сервера.

Infinispan:
Масштабируемый и распределенный кеш-провайдер, который может работать как в памяти, так и на диске.

Caffeine:
Высокопроизводительный кеш для Java, который поддерживает конфигурируемую политику истечения срока действия и другие функции.

Redis:
Распределенное хранилище данных, работающее в памяти, которое можно использовать для кеширования и обмена сообщениями.

Guava:
Библиотека от Google, которая предоставляет базовый механизм кеширования.

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
    }
}

Интеграция Spring Boot с AWS (Amazon Web Services) может включать множество различных сервисов, таких как Amazon S3, Amazon RDS, Amazon SQS и другие. В этом примере мы рассмотрим, как интегрироваться с Amazon S3 для загрузки и скачивания файлов.

Пример интеграции Spring Boot с 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)
    }
    
}

Непрерывная интеграция (Continuous Integration, CI) — это практика разработки программного обеспечения, при которой разработчики часто интегрируют изменения в основной репозиторий кода. Каждая интеграция проверяется автоматизированной сборкой и тестированием, что позволяет обнаружить и исправить ошибки на ранних этапах.

Пример настройки CI с использованием GitHub Actions для проекта на Spring Boot:

Создание репозитория на GitHub:
Создайте новый репозиторий на GitHub.
Клонируйте репозиторий на локальную машину и добавьте исходный код вашего Spring Boot проекта.

Добавление файла конфигурации GitHub Actions:
В корне вашего проекта создайте директорию .github/workflows и внутри нее создайте файл 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.

on:
Определяет триггеры для запуска workflow. В данном случае это push и pull_request на ветку main.

jobs:
Определяет набор заданий, которые будут выполнены.

build:
Имя задания, которое будет выполняться.

runs-on: ubuntu-latest:
Указывает, что задание будет выполняться на последней версии Ubuntu.

steps:
Определяет шаги для выполнения задания:

Checkout repository:
Клонирование репозитория.

Set up JDK 11:
Установка JDK 11.

Cache Gradle packages:
Кэширование зависимостей Gradle для ускорения последующих сборок.

Grant execute permission for gradlew:
Предоставление прав на выполнение файла gradlew.

Build with Gradle:
Сборка проекта с использованием Gradle.

Run tests:
Запуск тестов.

Copyright: Roman Kryvolapov