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:
Запуск тестів.