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
JPQL и 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) е високопроизводително key-value хранилище в паметта (in-memory), което поддържа множество структури от данни, като низове, списъци, множества, хешове и сортирани множества. 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), например методи за пакетни операции и методи за работа с флъшването (flush).
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: За автоматично стартиране на билд при push в хранилището.
Настройка на действията след билда:
Можете да добавите действия след билда, като изпращане на известия или внедряване на приложението.
Какво е 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(status.createSavepoint()) } }) }
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")) се запазва в кеша с ключ 1.
Следващо извикване на метода 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:
Клъстерен разпределен кеш, който може да се използва и като разпределена мрежа от данни в паметта (in-memory data grid).
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:
Стартиране на тестовете.