Java Spring — Хранение данных
Что такое ACID (Atomicity, Consistency, Isolation, Durability)
Набор свойств, которые гарантируют надёжность и предсказуемость транзакций в системах управления базами данных (СУБД). Аббревиатура расшифровывается как:
Atomicity (Атомарность)
Транзакция выполняется целиком или не выполняется вообще.
Если произошла ошибка на любом этапе, система откатывает все изменения, внесённые в рамках этой транзакции.
Пример:
если вы переводите деньги с одного счёта на другой, операция списания и операция зачисления должны пройти вместе. Если зачисление не удалось, то и списание отменяется.
Consistency (Согласованность)
После завершения транзакции база должна оставаться в корректном и согласованном состоянии.
Это означает соблюдение всех ограничений (уникальность, внешние ключи, правила и т. д.).
Пример:
если сумма на счёте не может быть отрицательной, то ни одна транзакция не должна привести к нарушению этого правила.
Isolation (Изолированность)
Одновременные транзакции не должны мешать друг другу.
Каждая транзакция должна выполняться так, будто она одна в системе.
СУБД использует уровни изоляции (Read Uncommitted, Read Committed, Repeatable Read, Serializable), чтобы управлять этим свойством.
Пример:
два пользователя не должны видеть промежуточные результаты друг друга.
Durability (Надёжность / Постоянство)
После успешного завершения транзакции её изменения не будут потеряны, даже если произойдёт сбой (например, отключение электричества).
СУБД записывает изменения в журнал или на диск, чтобы обеспечить восстановление.
Пример:
если операция перевода завершена, деньги точно "уйдут" и "придут" — даже при падении сервера.
Вместе эти свойства делают возможным надёжное управление данными, особенно в условиях высокой нагрузки или сбоев. Они — основа транзакционной логики в большинстве реляционных баз данных (PostgreSQL, MySQL, Oracle и др.).
Что такое CRUD (Create, Read, Update, Delete)
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.Entityimport javax.persistence.GeneratedValueimport javax.persistence.GenerationTypeimport javax.persistence.Id
@Entitydata class Product( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, val name: String, val price: Double)import org.springframework.data.jpa.repository.JpaRepositoryimport org.springframework.stereotype.Repository
@Repositoryinterface ProductRepository : JpaRepository<Product, Long> {
fun findByName(name: String): List<Product>
}import org.springframework.beans.factory.annotation.Autowiredimport 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() }}Что такое JPA (Java Persistence API) и Hibernate
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 Entityimport javax.persistence.Entity;import javax.persistence.Id;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;
@Entitypublic class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name;}
// JPA Repositoryimport 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 — это часть проекта 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:
Возможность создания пользовательских методов для выполнения специфических операций с базой данных.
Что такое JDBC (Java Database Connectivity)
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(); } }
}Чем JPA (Java Persistence API) отличается от JDBC (Java Database Connectivity)
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;
@Entitypublic 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;
@Servicepublic 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;
@RestControllerpublic 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 для сложных запросов.
Что такое JPQL и Criteria API
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.JpaRepositoryimport org.springframework.data.jpa.repository.Queryimport org.springframework.stereotype.Repository
@Repositoryinterface 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.Repositoryimport javax.persistence.EntityManagerimport javax.persistence.PersistenceContextimport javax.persistence.criteria.CriteriaBuilderimport javax.persistence.criteria.CriteriaQueryimport javax.persistence.criteria.Root
@Repositoryclass 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.JpaRepositoryimport org.springframework.stereotype.Repository
@Repositoryinterface UserRepository : JpaRepository<User, Long>, UserRepositoryCustomКак подключаться к базам данных в Spring
В Spring Boot можно легко настроить подключение к разным типам баз данных, используя файлы конфигурации application.properties. Ниже приведены примеры настройки подключения к различным популярным базам данных: H2, MySQL, PostgreSQL, Oracle, MariaDB.
Подключение к H2 (в памяти):
H2 — это встроенная база данных, которая часто используется для разработки и тестирования.
dependencies { implementation("com.h2database:h2")}# H2 Database Configurationspring.datasource.url=jdbc:h2:mem:testdbspring.datasource.driverClassName=org.h2.Driverspring.datasource.username=saspring.datasource.password=passwordspring.jpa.database-platform=org.hibernate.dialect.H2Dialectspring.h2.console.enabled=truespring.jpa.hibernate.ddl-auto=updateПодключение к MySQL:
dependencies { implementation("mysql:mysql-connector-java")}# MySQL Database Configurationspring.datasource.url=jdbc:mysql://localhost:3306/mydatabasespring.datasource.driverClassName=com.mysql.cj.jdbc.Driverspring.datasource.username=myuserspring.datasource.password=mypasswordspring.jpa.database-platform=org.hibernate.dialect.MySQLDialectspring.jpa.hibernate.ddl-auto=updateПодключение к PostgreSQL:
dependencies { implementation("org.postgresql:postgresql")}# PostgreSQL Database Configurationspring.datasource.url=jdbc:postgresql://localhost:5432/mydatabasespring.datasource.driverClassName=org.postgresql.Driverspring.datasource.username=myuserspring.datasource.password=mypasswordspring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialectspring.jpa.hibernate.ddl-auto=updateПодключение к Oracle:
dependencies { implementation("com.oracle.database.jdbc:ojdbc8")}# Oracle Database Configurationspring.datasource.url=jdbc:oracle:thin:@localhost:1521:orclspring.datasource.driverClassName=oracle.jdbc.OracleDriverspring.datasource.username=myuserspring.datasource.password=mypasswordspring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialectspring.jpa.hibernate.ddl-auto=updateПодключение к MongoDB (NoSQL):
dependencies { implementation("org.springframework.boot:spring-boot-starter-data-mongodb")}spring.data.mongodb.host=localhostspring.data.mongodb.port=27017spring.data.mongodb.database=mydatabaseimport org.springframework.data.annotation.Idimport 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.MongoRepositoryimport org.springframework.stereotype.Repository
@Repositoryinterface 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/mydatabasespring.datasource.username=rootspring.datasource.password=yourpasswordspring.datasource.driver-class-name=org.mariadb.jdbc.Driverspring.jpa.hibernate.ddl-auto=updatespring.jpa.show-sql=truespring.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.1spring.couchbase.username=your_usernamespring.couchbase.password=your_passwordspring.couchbase.bucket.name=your_bucketimport org.springframework.context.annotation.Configurationimport org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration
@Configurationclass 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.Userimport org.springframework.data.couchbase.repository.CouchbaseRepositoryimport org.springframework.stereotype.Repository
@Repositoryinterface UserRepository : CouchbaseRepository<User, String> { fun findByEmail(email: String): List<User>}Настройка пула соединений:
Независимо от используемой базы данных, можно настроить пул соединений для улучшения производительности.
# Connection Pool Configurationspring.datasource.hikari.maximum-pool-size=10spring.datasource.hikari.minimum-idle=5spring.datasource.hikari.idle-timeout=30000spring.datasource.hikari.max-lifetime=1800000spring.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.Serviceimport org.springframework.transaction.annotation.Isolationimport org.springframework.transaction.annotation.Transactional
@Serviceclass MyService { @Transactional(isolation = Isolation.READ_UNCOMMITTED) fun performReadUncommittedOperation() { }}Какие есть параметры у аннотации @Transactional
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.Serviceimport org.springframework.transaction.annotation.Isolationimport org.springframework.transaction.annotation.Propagationimport org.springframework.transaction.annotation.Transactional
@Serviceclass 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 в Spring
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=localhostspring.redis.port=6379import org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.data.redis.connection.RedisConnectionFactoryimport org.springframework.data.redis.core.RedisTemplateimport org.springframework.data.redis.serializer.GenericToStringSerializer
@Configurationclass 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) : Serializableimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.data.redis.core.RedisTemplateimport org.springframework.stereotype.Service
@Serviceclass 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.Autowiredimport 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-приложении вы можете использовать подходы для мониторинга изменений в базе данных и выполнения действий, таких как отправка запроса, при изменении значения в базе данных. Один из популярных способов достижения этого — использование триггеров в базе данных вместе с 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_triggerAFTER UPDATE ON your_tableFOR EACH ROWEXECUTE FUNCTION notify_table_update();Настройка слушателя в Spring Boot:
Используйте JDBC для прослушивания уведомлений от базы данных.
spring.datasource.url=jdbc:postgresql://localhost:5432/your_databasespring.datasource.username=your_usernamespring.datasource.password=your_passwordspring.datasource.driver-class-name=org.postgresql.Driverimport org.springframework.jdbc.core.JdbcTemplateimport org.springframework.stereotype.Serviceimport javax.annotation.PostConstruct
@Serviceclass 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_databaseimport com.mongodb.client.MongoClientsimport com.mongodb.client.model.changestream.ChangeStreamDocumentimport org.bson.Documentimport org.springframework.stereotype.Serviceimport javax.annotation.PostConstruct
@Serviceclass 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-запроса }
}Какие есть репозитории в Spring
Repository ├── CrudRepository │ ├── PagingAndSortingRepository │ │ ├── JpaRepository │ │ └── MongoRepository │ └── ReactiveCrudRepository │ └── R2dbcRepositoryCrudRepository:
предоставляет основные методы 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.ReactiveCrudRepositoryimport 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.R2dbcRepositoryimport reactor.core.publisher.Mono
interface PersonRepository : R2dbcRepository<Person, Long> {
fun findByLastName(lastName: String): Mono<Person>
}Что такое Jenkins
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
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)
План запроса (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
Пример создания индекса в 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:testdbspring.datasource.driverClassName=org.h2.Driverspring.datasource.username=saspring.datasource.password=passwordspring.jpa.database-platform=org.hibernate.dialect.H2Dialectspring.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.Serviceimport org.springframework.transaction.annotation.Isolationimport org.springframework.transaction.annotation.Transactional
@Serviceclass 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.Lockimport org.springframework.data.jpa.repository.Queryimport org.springframework.data.repository.CrudRepositoryimport 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.Lockimport org.springframework.data.jpa.repository.Queryimport org.springframework.data.repository.CrudRepositoryimport 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.Autowiredimport org.springframework.stereotype.Serviceimport org.springframework.transaction.annotation.Transactionalimport javax.persistence.EntityManagerimport javax.persistence.LockModeType
@Serviceclass 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
В Spring Framework управление транзакциями может быть реализовано несколькими способами. Основные способы включают декларативное и программное управление транзакциями.
Декларативное управление транзакциями:
@Transactional:
Используется для указания методов или классов, которые должны выполняться в рамках транзакции.
Можно настроить параметры, такие как уровень изоляции, распространение, тайм-аут и т.д.
import org.springframework.stereotype.Serviceimport org.springframework.transaction.annotation.Transactional
@Serviceclass 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.Autowiredimport org.springframework.stereotype.Serviceimport org.springframework.transaction.PlatformTransactionManagerimport org.springframework.transaction.TransactionDefinitionimport org.springframework.transaction.TransactionStatusimport org.springframework.transaction.support.DefaultTransactionDefinition
@Serviceclass 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.
@Beanfun transactionManager(dataSource: DataSource): PlatformTransactionManager { return DataSourceTransactionManager(dataSource)}JPA-транзакции:
Используется JpaTransactionManager.
Подходит для управления транзакциями на уровне JPA.
@Beanfun transactionManager(entityManagerFactory: EntityManagerFactory): PlatformTransactionManager { return JpaTransactionManager(entityManagerFactory)}Hibernate-транзакции:
Используется HibernateTransactionManager.
Подходит для управления транзакциями на уровне Hibernate.
@Beanfun transactionManager(sessionFactory: SessionFactory): PlatformTransactionManager { return HibernateTransactionManager(sessionFactory)}JTA-транзакции:
Используется JtaTransactionManager.
Подходит для управления распределенными транзакциями в корпоративных приложениях.
@Beanfun 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.Serviceimport org.springframework.transaction.support.TransactionCallbackimport org.springframework.transaction.support.TransactionCallbackWithoutResultimport org.springframework.transaction.support.TransactionTemplateimport org.springframework.transaction.TransactionStatus
@Serviceclass 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()) { // Текущая транзакция активна } }
}Что такое R2DBC (Reactive Relational Database Connectivity)
Для работы с базой данных в реактивном стиле в 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: mypasswordimport org.springframework.data.annotation.Idimport 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.ReactiveCrudRepositoryimport reactor.core.publisher.Mono
interface UserRepository : ReactiveCrudRepository<User, Long> {
fun findByEmail(email: String): Mono<User>
}import org.springframework.stereotype.Serviceimport reactor.core.publisher.Fluximport reactor.core.publisher.Mono
@Serviceclass 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.Fluximport 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:
Версионность миграций:
Каждая миграция идентифицируется уникальным номером версии, что позволяет отслеживать изменения в базе данных и гарантировать, что они применяются в правильном порядке.
Поддержка различных баз данных:
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: DEBUGspring: 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/migrationjwt: 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 приложениях приватные ключи и другие конфиденциальные данные должны храниться безопасно, чтобы предотвратить несанкционированный доступ. Вот несколько рекомендаций по их хранению:
Spring Boot Configuration Properties:
Используйте файл application.properties или application.yml, но не храните в них сами ключи, а лишь указывайте пути к ним.
Environment Variables:
Окружающие переменные (environment variables) являются безопасным способом хранения конфиденциальной информации.
@Value("\${my.secret.key}")private lateinit var mySecretKey: StringExternal 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: applicationJava Keystore (JKS):
Хранение ключей в JKS является хорошей практикой. Пример загрузки ключа из хранилища JKS:
import java.io.FileInputStreamimport 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 PrivateKeyAWS Secrets Manager / Azure Key Vault / Google Secret Manager:
Если ваше приложение разворачивается в облаке, используйте облачные сервисы для хранения секретов.
import com.amazonaws.services.secretsmanager.AWSSecretsManagerimport com.amazonaws.services.secretsmanager.model.GetSecretValueRequest
@Autowiredprivate 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.PooledPBEStringEncryptorimport org.jasypt.encryption.pbe.config.SimpleStringPBEConfigimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configuration
@Configurationclass JasyptConfig {
@Bean fun stringEncryptor(): PooledPBEStringEncryptor { val encryptor = PooledPBEStringEncryptor() val config = SimpleStringPBEConfig().apply { password = "encryption-password" algorithm = "PBEWithMD5AndDES" } encryptor.setConfig(config) return encryptor }
}Как в Spring можно использовать кеширование
Конфигурация кеша:
Включение кеширования в конфигурационном классе с помощью аннотации @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.EnableCachingimport org.springframework.context.annotation.Configuration
@Configuration@EnableCachingclass CacheConfig@Cacheable:
Кеширует результат метода.
@CachePut:
Обновляет кеш после выполнения метода.
@CacheEvict:
Удаляет записи из кеша.
@Serviceclass 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: simpleJCache (JSR-107) application.yml:
spring: cache: type: jcacheEhcache application.yml:
spring: cache: type: ehcacheEhcache 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: hazelcastHazelcast 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: infinispanInfinispan infinispan.xml:
<infinispan> <cache-container> <local-cache name="users"> <expiration lifespan="3600000"/> </local-cache> </cache-container></infinispan>Caffeine application.yml:
spring: cache: type: caffeineimport com.github.benmanes.caffeine.cache.Caffeineimport org.springframework.cache.CacheManagerimport org.springframework.cache.caffeine.CaffeineCacheManagerimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport java.util.concurrent.TimeUnit
@Configurationclass 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: 6379import org.springframework.cache.annotation.EnableCachingimport org.springframework.cache.redis.RedisCacheConfigurationimport org.springframework.cache.redis.RedisCacheManagerimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.data.redis.connection.RedisConnectionFactoryimport org.springframework.data.redis.core.RedisTemplateimport org.springframework.data.redis.serializer.RedisSerializationContextimport org.springframework.data.redis.serializer.StringRedisSerializer
@Configuration@EnableCachingclass 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: guavaimport com.google.common.cache.CacheBuilderimport org.springframework.cache.CacheManagerimport org.springframework.cache.guava.GuavaCacheManagerimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport java.util.concurrent.TimeUnit
@Configurationclass CacheConfig {
@Bean fun cacheManager(): CacheManager { val cacheManager = GuavaCacheManager("users") cacheManager.setCacheBuilder( CacheBuilder.newBuilder() .expireAfterWrite(60, TimeUnit.MINUTES) .maximumSize(100) ) return cacheManager }}Как использовать AWS (Amazon Web Services) в Spring
Интеграция 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_IDaws.secretKey=YOUR_SECRET_KEYaws.region=us-east-1aws.s3.bucketName=your-bucket-nameimport org.springframework.beans.factory.annotation.Valueimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport software.amazon.awssdk.auth.credentials.AwsBasicCredentialsimport software.amazon.awssdk.auth.credentials.StaticCredentialsProviderimport software.amazon.awssdk.regions.Regionimport software.amazon.awssdk.services.s3.S3Client
@Configurationclass 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.Valueimport org.springframework.stereotype.Serviceimport org.springframework.web.multipart.MultipartFileimport software.amazon.awssdk.services.s3.S3Clientimport software.amazon.awssdk.services.s3.model.PutObjectRequestimport software.amazon.awssdk.services.s3.model.GetObjectRequestimport java.nio.file.Paths
@Serviceclass 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.HttpHeadersimport org.springframework.http.MediaTypeimport org.springframework.http.ResponseEntityimport 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) }
}Как настроить GitHub Actions Continuous Integration
Непрерывная интеграция (Continuous Integration, CI) — это практика разработки программного обеспечения, при которой разработчики часто интегрируют изменения в основной репозиторий кода. Каждая интеграция проверяется автоматизированной сборкой и тестированием, что позволяет обнаружить и исправить ошибки на ранних этапах.
Пример настройки CI с использованием GitHub Actions для проекта на Spring Boot:
Создание репозитория на GitHub:
Создайте новый репозиторий на GitHub.
Клонируйте репозиторий на локальную машину и добавьте исходный код вашего Spring Boot проекта.
Добавление файла конфигурации GitHub Actions:
В корне вашего проекта создайте директорию .github/workflows и внутри нее создайте файл ci.yml.
.github/workflows/ci.yml
name: CIon: 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 testname: 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:
Запуск тестов.