get(ID id) {
* If entity has ID and record with this ID already exists in DB, CONFLICT http status and Location header was returned.
* Otherwise, CREATE http status will be returned with Location header.
* When creating new object, ID may be passed, but that ID only used when no {@link GeneratedValue} set
- * on Entity ID field or if {@link GeneratedValue#generator} set to {@link org.hibernate.id.Assigned},
- * {@link ru.investbook.entity.AssignedOrIdentityGenerator} or similar generator impl;
- * otherwise ID, passed in object, will be ignored (see JPA impl).
+ * on Entity ID field or if {@link GeneratedValue#generator} set to {@link org.hibernate.generator.BeforeExecutionGenerator},
+ * generator impl; otherwise ID, passed in object, will be ignored (see JPA impl).
*
* @param object new entity (ID may be missed)
* @throws InternalServerErrorException if object not created or updated
diff --git a/src/main/java/ru/investbook/entity/AssignedOrGeneratedValue.java b/src/main/java/ru/investbook/entity/AssignedOrGeneratedValue.java
index 1a2819a4..a5c0471a 100644
--- a/src/main/java/ru/investbook/entity/AssignedOrGeneratedValue.java
+++ b/src/main/java/ru/investbook/entity/AssignedOrGeneratedValue.java
@@ -18,7 +18,6 @@
package ru.investbook.entity;
-import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.hibernate.annotations.IdGeneratorType;
@@ -32,13 +31,14 @@
/**
* Provides ID generation strategy for the values of primary keys.
* The annotation may be applied to a primary key property with the {@link Id} annotation.
- * Behaves like {@link GeneratedValue} if primary key is null or use assigned value otherwise.
+ * Annotation behaves like {@code @GeneratedValue(strategy = IDENTITY)} if primary key is null.
+ * If primary key value is not null it will be inserted to DB.
*
* Annotation helps to replace code snippet
*
* @Id
- * @GeneratedValue(generator = "generator-name")
- * @GenericGenerator(name = "generator-name", type = AssignedOrIdentityGenerator.class)
+ * @GeneratedValue(generator = "use-assigned-if-present")
+ * @GenericGenerator(name = "use-assigned-if-present", type = AssignedOrIdentityGenerator.class)
* Integer id;
*
* with shorter one
@@ -50,6 +50,7 @@
*
*
* @see jakarta.persistence.GeneratedValue
+ * @see AssignedOrIdentityGenerator
*/
@Retention(RUNTIME)
@Target({METHOD, FIELD})
diff --git a/src/main/java/ru/investbook/entity/AssignedOrIdentityGenerator.java b/src/main/java/ru/investbook/entity/AssignedOrIdentityGenerator.java
index f866a307..b0f8b442 100644
--- a/src/main/java/ru/investbook/entity/AssignedOrIdentityGenerator.java
+++ b/src/main/java/ru/investbook/entity/AssignedOrIdentityGenerator.java
@@ -19,46 +19,24 @@
package ru.investbook.entity;
import jakarta.persistence.Id;
-import org.hibernate.engine.spi.SharedSessionContractImplementor;
-import org.hibernate.generator.BeforeExecutionGenerator;
-import org.hibernate.generator.EventType;
+import org.hibernate.id.Assigned;
import org.hibernate.id.IdentityGenerator;
-import static java.util.Objects.isNull;
-
/**
- * Behaves like {@link org.hibernate.id.Assigned} generator if entity {@link Id} is not null
- * or like {@link org.hibernate.id.IdentityGenerator} otherwise.
+ * If Entity ID is not null, then this ID is stored to DB.
+ * If Entity ID is null, then RDBM should generate ID itself.
+ *
+ * In other words: this generator behaves like {@link org.hibernate.id.Assigned} generator if entity's field,
+ * marked by {@link Id} annotation, is not null, or like {@link org.hibernate.id.IdentityGenerator} if this field is null.
+ *
+ * Applicable only for INSERT operations.
*/
-// IdentityGenerator can be replaced by SelectGenerator if RDBMS doesn't support AUTO_INCREMENT/IDENTITY fields.
-public class AssignedOrIdentityGenerator extends IdentityGenerator implements BeforeExecutionGenerator {
-
- @Override
- public Object generate(SharedSessionContractImplementor session, Object entity, Object currentValue, EventType eventType) {
- return getId(entity, session);
- }
-
- private static Object getId(Object entity, SharedSessionContractImplementor session) {
- return session.getEntityPersister(null, entity)
- .getIdentifier(entity, session);
- }
-
- /**
- * @implNote Method {@link BeforeExecutionGenerator#generate(SharedSessionContractImplementor, Object, Object, EventType)}
- * is called if this method returns false
- */
- @Override
- public boolean generatedOnExecution(Object entity, SharedSessionContractImplementor session) {
- Object id = getId(entity, session);
- return isNull(id);
- }
+class AssignedOrIdentityGenerator extends BeforeOrOnExecutionGenerator {
- @Override
- public boolean generatedOnExecution() {
- // This method is called to configure a context (using this Generator) without knowledge of a specific Entity.
- // The choice for the real Entity must be made in the this.generatedOnExecution(entity, session) method.
- // For example, find out comment "support mixed-timing generators" in IdentifierGeneratorUtil.class (hibernate-core):
- // true is required, if ID sometimes should be generated by RDBMS (for example by AUTO_INCREMENT)
- return true;
+ @SuppressWarnings("unused")
+ public AssignedOrIdentityGenerator() {
+ // IdentityGenerator can be replaced by SelectGenerator
+ // if RDBMS doesn't support AUTO_INCREMENT/IDENTITY fields
+ super(new Assigned(), new IdentityGenerator());
}
}
diff --git a/src/main/java/ru/investbook/entity/BeforeOrOnExecutionGenerator.java b/src/main/java/ru/investbook/entity/BeforeOrOnExecutionGenerator.java
new file mode 100644
index 00000000..fd2d1545
--- /dev/null
+++ b/src/main/java/ru/investbook/entity/BeforeOrOnExecutionGenerator.java
@@ -0,0 +1,112 @@
+/*
+ * InvestBook
+ * Copyright (C) 2024 Spacious Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package ru.investbook.entity;
+
+import lombok.Getter;
+import org.hibernate.dialect.Dialect;
+import org.hibernate.engine.spi.SharedSessionContractImplementor;
+import org.hibernate.generator.BeforeExecutionGenerator;
+import org.hibernate.generator.EventType;
+import org.hibernate.generator.OnExecutionGenerator;
+import org.hibernate.id.PostInsertIdentityPersister;
+import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate;
+import org.hibernate.persister.entity.EntityPersister;
+
+import java.util.EnumSet;
+
+import static java.util.Objects.isNull;
+
+
+/**
+ * Allows to determine at runtime (based on Entity for example) whether an ID should be generated on client or on DB server side.
+ * To implement this constructor accepts {@link BeforeExecutionGenerator} and {@link OnExecutionGenerator} ID generators.
+ *
+ * If java {@link BeforeExecutionGenerator#generate(SharedSessionContractImplementor, Object entity, Object, EventType)}
+ * returns not null ID, when this ID will be stored to DB. In another case, the DBMS must generate ID itself
+ * based on the strategy implemented by {@link OnExecutionGenerator}.
+ */
+class BeforeOrOnExecutionGenerator implements BeforeExecutionGenerator, OnExecutionGenerator {
+
+ private final BeforeExecutionGenerator beforeExecutionGenerator;
+ private final OnExecutionGenerator onExecutionGenerator;
+ @Getter
+ private final EnumSet eventTypes;
+
+ public BeforeOrOnExecutionGenerator(BeforeExecutionGenerator beforeExecutionGenerator,
+ OnExecutionGenerator onExecutionGenerator) {
+ this.beforeExecutionGenerator = beforeExecutionGenerator;
+ this.onExecutionGenerator = onExecutionGenerator;
+ this.eventTypes = EnumSet.copyOf(beforeExecutionGenerator.getEventTypes());
+ eventTypes.addAll(onExecutionGenerator.getEventTypes());
+ }
+
+ @Override
+ public boolean generatedOnExecution() {
+ // This method is called to configure a context (using this Generator) without knowledge of a specific Entity.
+ // The choice for the real Entity must be made in the this.generatedOnExecution(entity, session) method.
+ // For example, find out comment "support mixed-timing generators" in IdentifierGeneratorUtil.class (hibernate-core):
+ // true is required, if this Generator want sometimes generate ID by RDBMS (for example by AUTO_INCREMENT)
+ return true;
+ }
+
+ /**
+ * @implNote If this method returns false,
+ * then {@link #generate(SharedSessionContractImplementor, Object, Object, EventType)} is called to get ID
+ */
+ @Override
+ public boolean generatedOnExecution(Object entity, SharedSessionContractImplementor session) {
+ try {
+ EventType eventType = beforeExecutionGenerator.getEventTypes().iterator().next();
+ Object id = beforeExecutionGenerator.generate(session, entity, null, eventType);
+ return isNull(id);
+ } catch (Exception e) {
+ return true; // RDBMS should generate ID
+ }
+ }
+
+ @Override
+ public Object generate(SharedSessionContractImplementor session, Object entity, Object currentValue, EventType eventType) {
+ return beforeExecutionGenerator.generate(session, entity, currentValue, eventType);
+ }
+
+ @Override
+ public boolean referenceColumnsInSql(Dialect dialect) {
+ return onExecutionGenerator.referenceColumnsInSql(dialect);
+ }
+
+ @Override
+ public boolean writePropertyValue() {
+ return onExecutionGenerator.writePropertyValue();
+ }
+
+ @Override
+ public String[] getReferencedColumnValues(Dialect dialect) {
+ return onExecutionGenerator.getReferencedColumnValues(dialect);
+ }
+
+ @Override
+ public InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate(PostInsertIdentityPersister persister) {
+ return onExecutionGenerator.getGeneratedIdentifierDelegate(persister);
+ }
+
+ @Override
+ public String[] getUniqueKeyPropertyNames(EntityPersister persister) {
+ return onExecutionGenerator.getUniqueKeyPropertyNames(persister);
+ }
+}
From d244a8da5241fc2bca4fd4d361c649ca6fb463b5 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sun, 21 Apr 2024 20:32:16 +0300
Subject: [PATCH 09/40] refactor some classes
---
pom.xml | 2 +-
.../parser/BrokerReportParserServiceImpl.java | 31 +++++++++----------
.../parser/MailboxReportParserService.java | 5 ++-
.../PsbBrokerForeignMarketReport.java | 4 +--
4 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/pom.xml b/pom.xml
index c594ea9b..e0c35c0c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -210,7 +210,7 @@
org.projectlombok
lombok
- 1.18.30
+ 1.18.32
true
diff --git a/src/main/java/ru/investbook/parser/BrokerReportParserServiceImpl.java b/src/main/java/ru/investbook/parser/BrokerReportParserServiceImpl.java
index 883da8ec..93af7c0a 100644
--- a/src/main/java/ru/investbook/parser/BrokerReportParserServiceImpl.java
+++ b/src/main/java/ru/investbook/parser/BrokerReportParserServiceImpl.java
@@ -31,7 +31,6 @@
import ru.investbook.InvestbookProperties;
import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
@@ -58,19 +57,18 @@ public class BrokerReportParserServiceImpl implements BrokerReportParserService
@SneakyThrows
@Override
public void parseReport(InputStream inputStream, String fileName, String broker) {
- try (ByteArrayInputStream is = castToByteArrayInputStream(inputStream)) {
- long t0 = System.nanoTime();
- is.mark(Integer.MAX_VALUE);
- String brokerName = parseReport0(is, fileName, broker);
- if (investbookProperties.isReportBackup()) {
- is.reset();
- Path path = saveToBackup(is, fileName, brokerName);
- log.info("Загрузка отчета {} завершена за {}, бекап отчета сохранен в {}",
- fileName, Duration.ofNanos(System.nanoTime() - t0), path.toAbsolutePath());
- } else {
- log.info("Загрузка отчета {} завершена за {}, бекап отключен конфигурацией",
- fileName, Duration.ofNanos(System.nanoTime() - t0));
- }
+ ByteArrayInputStream is = castToByteArrayInputStream(inputStream);
+ long t0 = System.nanoTime();
+ is.mark(Integer.MAX_VALUE);
+ String brokerName = parseReport0(is, fileName, broker);
+ if (investbookProperties.isReportBackup()) {
+ is.reset();
+ Path path = saveToBackup(is, fileName, brokerName);
+ log.info("Загрузка отчета {} завершена за {}, бекап отчета сохранен в {}",
+ fileName, Duration.ofNanos(System.nanoTime() - t0), path.toAbsolutePath());
+ } else {
+ log.info("Загрузка отчета {} завершена за {}, бекап отключен конфигурацией",
+ fileName, Duration.ofNanos(System.nanoTime() - t0));
}
}
@@ -78,9 +76,8 @@ public static ByteArrayInputStream castToByteArrayInputStream(InputStream inputS
if (inputStream instanceof ByteArrayInputStream) {
return (ByteArrayInputStream) inputStream;
} else {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- inputStream.transferTo(out);
- return new ByteArrayInputStream(out.toByteArray());
+ byte[] bytes = inputStream.readAllBytes();
+ return new ByteArrayInputStream(bytes);
}
}
diff --git a/src/main/java/ru/investbook/parser/MailboxReportParserService.java b/src/main/java/ru/investbook/parser/MailboxReportParserService.java
index 65c20d4a..c282cc9c 100644
--- a/src/main/java/ru/investbook/parser/MailboxReportParserService.java
+++ b/src/main/java/ru/investbook/parser/MailboxReportParserService.java
@@ -38,6 +38,7 @@
import org.springframework.stereotype.Component;
import ru.investbook.web.model.MailboxDescriptor;
+import java.io.InputStream;
import java.time.Duration;
import java.util.Properties;
import java.util.stream.Stream;
@@ -140,7 +141,9 @@ private boolean handleBodyPart(BodyPart bodyPart, MailboxDescriptor mailbox) {
if (bodyPart.getFileName() != null) {
DataHandler dataHandler = bodyPart.getDataHandler();
DataSource dataSource = dataHandler.getDataSource();
- brokerReportParserService.parseReport(dataSource.getInputStream(), dataSource.getName(), mailbox.getBroker());
+ try (InputStream inputStream = dataSource.getInputStream()) {
+ brokerReportParserService.parseReport(inputStream, dataSource.getName(), mailbox.getBroker());
+ }
return true;
}
} catch (Exception e) {
diff --git a/src/main/java/ru/investbook/parser/psb/foreignmarket/PsbBrokerForeignMarketReport.java b/src/main/java/ru/investbook/parser/psb/foreignmarket/PsbBrokerForeignMarketReport.java
index 60930588..c9de2950 100644
--- a/src/main/java/ru/investbook/parser/psb/foreignmarket/PsbBrokerForeignMarketReport.java
+++ b/src/main/java/ru/investbook/parser/psb/foreignmarket/PsbBrokerForeignMarketReport.java
@@ -60,14 +60,14 @@ public PsbBrokerForeignMarketReport(String excelFileName, InputStream is, Securi
private static Workbook getWorkbook(InputStream is) {
try {
ExcelReader reader = new ExcelReader();
- is = skeepNewLines(is); // required by ExcelReader
+ is = skipNewLines(is); // required by ExcelReader
return reader.getWorkbook(new InputSource(is));
} catch (Exception e) {
throw new RuntimeException("Не смог открыть xml файл", e);
}
}
- private static InputStream skeepNewLines(InputStream is) throws IOException {
+ private static InputStream skipNewLines(InputStream is) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(is.readAllBytes());
int symbol;
do {
From a93b780f45acb6d4c0322411f1c024f5c510683f Mon Sep 17 00:00:00 2001
From: vananiev
Date: Tue, 23 Apr 2024 00:03:10 +0300
Subject: [PATCH 10/40] update spring boot to 3.2.5
---
pom.xml | 3 +--
.../java/ru/investbook/entity/AssignedOrIdentityGenerator.java | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/pom.xml b/pom.xml
index e0c35c0c..69ecc818 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,7 +23,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.0.7
+ 3.2.5
ru.investbook
@@ -64,7 +64,6 @@
24.2
21
- 6.4.4.Final
diff --git a/src/main/java/ru/investbook/entity/AssignedOrIdentityGenerator.java b/src/main/java/ru/investbook/entity/AssignedOrIdentityGenerator.java
index b0f8b442..b3b14b45 100644
--- a/src/main/java/ru/investbook/entity/AssignedOrIdentityGenerator.java
+++ b/src/main/java/ru/investbook/entity/AssignedOrIdentityGenerator.java
@@ -31,7 +31,7 @@
*
* Applicable only for INSERT operations.
*/
-class AssignedOrIdentityGenerator extends BeforeOrOnExecutionGenerator {
+public class AssignedOrIdentityGenerator extends BeforeOrOnExecutionGenerator {
@SuppressWarnings("unused")
public AssignedOrIdentityGenerator() {
From 6dd5399eef6b311d393e7d383747a78183beb722 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Tue, 23 Apr 2024 00:08:18 +0300
Subject: [PATCH 11/40] fix spring boot update (#593)
---
pom.xml | 1 +
1 file changed, 1 insertion(+)
diff --git a/pom.xml b/pom.xml
index 69ecc818..83264eb2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -171,6 +171,7 @@
com.h2database
h2
+ 2.1.214
runtime
From 574a1ee093031836827f5a61d55d0bf0762294dc Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 22 Apr 2024 21:08:47 +0000
Subject: [PATCH 12/40] Bump com.h2database:h2 from 2.1.214 to 2.2.220
Bumps [com.h2database:h2](https://github.com/h2database/h2database) from 2.1.214 to 2.2.220.
- [Release notes](https://github.com/h2database/h2database/releases)
- [Commits](https://github.com/h2database/h2database/compare/version-2.1.214...version-2.2.220)
---
updated-dependencies:
- dependency-name: com.h2database:h2
dependency-type: direct:production
...
Signed-off-by: dependabot[bot]
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 83264eb2..1d5ded13 100644
--- a/pom.xml
+++ b/pom.xml
@@ -171,7 +171,7 @@
com.h2database
h2
- 2.1.214
+ 2.2.220
runtime
From 212a192db254f0e602d50d7b60ce76baa5c1902b Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sat, 4 May 2024 16:32:27 +0300
Subject: [PATCH 13/40] update h2 to 2.2.224 and import db
---
pom.xml | 1 -
src/main/java/ru/investbook/InvestbookProperties.java | 4 +++-
src/main/resources/application-dev.properties | 2 +-
src/main/resources/application-h2.properties | 2 +-
4 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/pom.xml b/pom.xml
index 1d5ded13..69ecc818 100644
--- a/pom.xml
+++ b/pom.xml
@@ -171,7 +171,6 @@
com.h2database
h2
- 2.2.220
runtime
diff --git a/src/main/java/ru/investbook/InvestbookProperties.java b/src/main/java/ru/investbook/InvestbookProperties.java
index cc6ddab3..d5a7ccf4 100644
--- a/src/main/java/ru/investbook/InvestbookProperties.java
+++ b/src/main/java/ru/investbook/InvestbookProperties.java
@@ -38,7 +38,9 @@ public class InvestbookProperties {
private Path reportBackupPath = dataPath.resolve("report-backups");
- private List sqlImportFiles = List.of(dataPath.resolve("export-2022.9.sql"));
+ private List sqlImportFiles = List.of(
+ dataPath.resolve("export-2022.9.sql"),
+ dataPath.resolve("export-2024.1.x.sql"));
private boolean openHomePageAfterStart = false;
diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties
index 8d2dd927..f901f26e 100644
--- a/src/main/resources/application-dev.properties
+++ b/src/main/resources/application-dev.properties
@@ -24,7 +24,7 @@
#spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
# H2
-spring.datasource.url=jdbc:h2:file:~/investbook2-test;mode=mysql;non_keywords=value
+spring.datasource.url=jdbc:h2:file:~/investbook3-test;mode=mysql;non_keywords=value
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.datasource.username = sa
spring.datasource.password =
diff --git a/src/main/resources/application-h2.properties b/src/main/resources/application-h2.properties
index b1dce744..b0454f1f 100644
--- a/src/main/resources/application-h2.properties
+++ b/src/main/resources/application-h2.properties
@@ -16,7 +16,7 @@
# along with this program. If not, see .
#
-spring.datasource.url=jdbc:h2:file:~/investbook/investbook2;mode=mysql;non_keywords=value
+spring.datasource.url=jdbc:h2:file:~/investbook/investbook3;mode=mysql;non_keywords=value
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.datasource.username = sa
spring.datasource.password =
From 2079f1681cd89808a80efaa613078f97005337ec Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sun, 19 May 2024 22:45:07 +0300
Subject: [PATCH 14/40] update spring boot version in readme
---
README-en.md | 2 +-
README.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/README-en.md b/README-en.md
index d9290aa9..0627e3ef 100644
--- a/README-en.md
+++ b/README-en.md
@@ -2,7 +2,7 @@
[](README.md)
[![java-version](https://img.shields.io/badge/java-21-brightgreen?style=flat-square)](https://openjdk.org/)
-[![spring-boot-version](https://img.shields.io/badge/spring--boot-3.0.7-brightgreen?style=flat-square)](https://github.com/spring-projects/spring-boot/releases)
+[![spring-boot-version](https://img.shields.io/badge/spring--boot-3.2.5-brightgreen?style=flat-square)](https://github.com/spring-projects/spring-boot/releases)
[![hits-of-code](https://img.shields.io/badge/dynamic/json?style=flat-square&color=lightblue&label=hits-of-code&url=https://hitsofcode.com/github/spacious-team/investbook/json?branch=develop&query=$.count)](https://hitsofcode.com/github/spacious-team/investbook/view?branch=develop)
[![github-closed-pull-requests](https://img.shields.io/github/issues-pr-closed/spacious-team/investbook?style=flat-square&color=brightgreen)](https://github.com/spacious-team/investbook/pulls?q=is%3Apr+is%3Aclosed)
[![github-workflow-status](https://img.shields.io/github/actions/workflow/status/spacious-team/investbook/publish-docker.yml?style=flat-square&branch=master)](https://github.com/spacious-team/investbook/actions/workflows/publish-docker.yml)
diff --git a/README.md b/README.md
index de924170..e9cb80a7 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](README.md)
[![java-version](https://img.shields.io/badge/java-21-brightgreen?style=flat-square)](https://openjdk.org/)
-[![spring-boot-version](https://img.shields.io/badge/spring--boot-3.0.7-brightgreen?style=flat-square)](https://github.com/spring-projects/spring-boot/releases)
+[![spring-boot-version](https://img.shields.io/badge/spring--boot-3.2.5-brightgreen?style=flat-square)](https://github.com/spring-projects/spring-boot/releases)
[![hits-of-code](https://img.shields.io/badge/dynamic/json?style=flat-square&color=lightblue&label=hits-of-code&url=https://hitsofcode.com/github/spacious-team/investbook/json?branch=develop&query=$.count)](https://hitsofcode.com/github/spacious-team/investbook/view?branch=develop)
[![github-closed-pull-requests](https://img.shields.io/github/issues-pr-closed/spacious-team/investbook?style=flat-square&color=brightgreen)](https://github.com/spacious-team/investbook/pulls?q=is%3Apr+is%3Aclosed)
[![github-workflow-status](https://img.shields.io/github/actions/workflow/status/spacious-team/investbook/publish-docker.yml?style=flat-square&branch=master)](https://github.com/spacious-team/investbook/actions/workflows/publish-docker.yml)
From 125fa4b5d69000a9a80f4f27fe860c7c31224dc3 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sun, 19 May 2024 22:51:18 +0300
Subject: [PATCH 15/40] update paketo-buildpacks/java
---
pom.xml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/pom.xml b/pom.xml
index 7ff4145e..de37b237 100644
--- a/pom.xml
+++ b/pom.xml
@@ -427,8 +427,7 @@
-
- gcr.io/paketo-buildpacks/java:10.2.1
+ gcr.io/paketo-buildpacks/java:14.0.0
From 2c30ad1ac4e0a07525c5ab58b49233e7c823d3f4 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Mon, 20 May 2024 00:53:15 +0300
Subject: [PATCH 16/40] update readme
---
README-en.md | 9 ++++-----
README.md | 9 ++++-----
2 files changed, 8 insertions(+), 10 deletions(-)
diff --git a/README-en.md b/README-en.md
index badb9234..86373d45 100644
--- a/README-en.md
+++ b/README-en.md
@@ -169,16 +169,15 @@ But the license also allows any developer to improve their own copy of the appli
will be open on the Internet).
### Why is the application code open source
-The idea of open source is the freedom to develop and use software.
-Many famous brands use open source, [Instagram](https://github.com/Instagram) for example,
+For some areas, open source solutions are better suited than others, for example in the fields of finance and data
+encryption, because these solutions can be trusted because you or anyone else can look at the code and see
+in the security of the program. Many famous brands use open source, [Instagram](https://github.com/Instagram) for example,
[Android](https://ru.wikipedia.org/wiki/Android#%D0%98%D1%81%D1%85%D0%BE%D0%B4%D0%BD%D1%8B%D0%B9_%D0%BA%D0%BE%D0%B4),
[Telegram](https://ru.wikipedia.org/wiki/Telegram), [Twitter](https://opensource.twitter.dev/),
[Google Chrome](https://ru.wikipedia.org/wiki/Google_Chrome),
[Mozilla Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Source_Code/Downloading_Source_Archives),
sites with a secure [https](https://ru.wikipedia.org/wiki/OpenSSL) connection, such as https://vk.com, etc.
-For some areas, open source solutions are better suited than others, for example in the fields of finance and data
-encryption, because these solutions can be trusted because you or anyone else can look at the code and see
-in the security of the program.
+The idea of open source is the freedom to develop and use software.
Elon Musk's opinion on open source.
diff --git a/README.md b/README.md
index e06431b6..915948f9 100644
--- a/README.md
+++ b/README.md
@@ -159,16 +159,15 @@ Investbook также может быть запущен в [docker](docs/run-by
будет открыт в сети интернет).
### Почему код приложения открыт
-Идея открытого исходного кода (open source) заключается в свободе разработки и использования программного обеспечения.
-Многие известные бренды используют open source, например [Instagram](https://github.com/Instagram),
+Для некоторых сфер решения с открытым исходным кодом подходят лучше других, например в сферах финансов и шифрования данных,
+т.к. этим решениям можно доверять вследствие того, что вы или любой другой желающий может посмотреть код и убедиться
+в безопасности программы. Многие известные бренды используют open source, например [Instagram](https://github.com/Instagram),
[Android](https://ru.wikipedia.org/wiki/Android#%D0%98%D1%81%D1%85%D0%BE%D0%B4%D0%BD%D1%8B%D0%B9_%D0%BA%D0%BE%D0%B4),
[Telegram](https://ru.wikipedia.org/wiki/Telegram), [Twitter](https://opensource.twitter.dev/),
[Google Chrome](https://ru.wikipedia.org/wiki/Google_Chrome),
[Mozilla Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Source_Code/Downloading_Source_Archives),
сайты с защищенным соединением [https](https://ru.wikipedia.org/wiki/OpenSSL), такие как https://vk.com и др.
-Для некоторых сфер решения с открытым исходным кодом подходят лучше других, например в сферах финансов и шифрования данных,
-т.к. этим решениям можно доверять вследствие того, что вы или любой другой желающий может посмотреть код и убедиться
-в безопасности программы.
+Идея открытого исходного кода (open source) заключается в свободе разработки и использования программного обеспечения.
Мнение Илона Маска об открытом исходном коде.
From 419c197821e1fd4e7698cf408da6e95d0aa8142f Mon Sep 17 00:00:00 2001
From: vananiev
Date: Tue, 21 May 2024 22:43:00 +0300
Subject: [PATCH 17/40] refactor AbstractRestController
---
.../api/AbstractRestController.java | 30 ++++++++++++-------
.../api/SecurityRestController.java | 6 ++--
...AbstractCbrForeignExchangeRateService.java | 2 +-
3 files changed, 23 insertions(+), 15 deletions(-)
diff --git a/src/main/java/ru/investbook/api/AbstractRestController.java b/src/main/java/ru/investbook/api/AbstractRestController.java
index e43c0b59..ed7f0724 100644
--- a/src/main/java/ru/investbook/api/AbstractRestController.java
+++ b/src/main/java/ru/investbook/api/AbstractRestController.java
@@ -76,16 +76,18 @@ protected ResponseEntity post(Pojo object) {
try {
ID id = getId(object);
if (id == null) {
- return createEntity(object);
+ return createOrUpdateEntity(object);
}
- Optional result = getById(id);
- if (result.isPresent()) {
+ if (existsById(id)) {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.location(getLocationURI(object))
.build();
} else {
- return createEntity(object);
+ // Если убрать @Transactional над методом, то следующая строка может обновить объект по ошибке.
+ // Это возможно, если объект был создан другим потоком после проверки существования строки по ID.
+ // По этой причине @Transactional убирать не нужно.
+ return createOrUpdateEntity(object);
}
} catch (Exception e) {
throw new InternalServerErrorException("Не могу создать объект", e);
@@ -114,31 +116,37 @@ public ResponseEntity put(ID id, Pojo object) {
throw new BadRequestException("Идентификатор объекта, переданный в URI [" + id + "] и в теле " +
"запроса [" + getId(object) + "] не совпадают");
}
- Optional result = getById(id);
- if (result.isPresent()) {
+ if (existsById(id)) {
saveAndFlush(object);
return ResponseEntity.ok().build();
} else {
- return createEntity(object);
+ return createOrUpdateEntity(object);
}
} catch (Exception e) {
throw new InternalServerErrorException("Не могу создать объект", e);
}
}
+ private boolean existsById(ID id) {
+ return repository.existsById(id);
+ }
+
protected abstract Pojo updateId(ID id, Pojo object);
private Entity saveAndFlush(Pojo object) {
- return repository.saveAndFlush(converter.toEntity(object));
+ Entity entity = converter.toEntity(object);
+ return repository.saveAndFlush(entity);
}
/**
* @return response entity with http CREATE status, Location http header and body
*/
- private ResponseEntity createEntity(Pojo object) throws URISyntaxException {
+ private ResponseEntity createOrUpdateEntity(Pojo object) throws URISyntaxException {
Entity entity = saveAndFlush(object);
+ Pojo savedObject = converter.fromEntity(entity);
+ URI locationURI = getLocationURI(savedObject);
return ResponseEntity
- .created(getLocationURI(converter.fromEntity(entity)))
+ .created(locationURI)
.build();
}
@@ -152,6 +160,6 @@ protected URI getLocationURI(Pojo object) throws URISyntaxException {
* Delete object from storage. Always return OK http status with empty body.
*/
public void delete(ID id) {
- getById(id).ifPresent(repository::delete);
+ repository.deleteById(id);
}
}
diff --git a/src/main/java/ru/investbook/api/SecurityRestController.java b/src/main/java/ru/investbook/api/SecurityRestController.java
index 7ec8e135..78480b5c 100644
--- a/src/main/java/ru/investbook/api/SecurityRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityRestController.java
@@ -65,7 +65,7 @@ public Page get(@Parameter(hidden = true)
@Override
@GetMapping("{id}")
@Operation(summary = "Отобразить один",
- description = "Отобразить биржевой инструмент по идентификатору (ISIN, коду дериватива, валютной пары)")
+ description = "Отобразить биржевой инструмент по внутреннему идентификатору")
public ResponseEntity get(@PathVariable("id")
@Parameter(description = "Идентификатор", example = "123", required = true)
Integer id) {
@@ -102,8 +102,8 @@ public void delete(@PathVariable("id")
}
@Override
- protected Optional getById(Integer isin) {
- return repository.findById(isin);
+ protected Optional getById(Integer id) {
+ return repository.findById(id);
}
@Override
diff --git a/src/main/java/ru/investbook/service/cbr/AbstractCbrForeignExchangeRateService.java b/src/main/java/ru/investbook/service/cbr/AbstractCbrForeignExchangeRateService.java
index fc9bef10..d9b2625e 100644
--- a/src/main/java/ru/investbook/service/cbr/AbstractCbrForeignExchangeRateService.java
+++ b/src/main/java/ru/investbook/service/cbr/AbstractCbrForeignExchangeRateService.java
@@ -78,7 +78,7 @@ protected void save(ForeignExchangeRate fxRate) {
ForeignExchangeRateEntity entity = foreignExchangeRateConverter.toEntity(fxRate);
foreignExchangeRateRepository.save(entity);
} catch (Exception e) {
- log.debug("Ошибка сохранения {}, может быть запись уже существует?", fxRate);
+ log.debug("Ошибка сохранения {}", fxRate);
}
}
}
From c19166a416c3456045791ea3e8c47734daa07395 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Wed, 22 May 2024 01:24:47 +0300
Subject: [PATCH 18/40] replace saveAndFlush() in transaction by save() in
transaction
---
.../api/AbstractRestController.java | 16 ++++-----
.../parser/SecurityRegistrarImpl.java | 12 +++----
.../AbstractSecurityAwareInvestbookTable.java | 2 +-
...bookSecurityDepositAndWithdrawalTable.java | 2 +-
.../service/EventCashFlowFormsService.java | 8 ++---
.../ForeignExchangeRateFormsService.java | 2 +-
.../service/PortfolioCashFormsService.java | 6 ++--
.../PortfolioPropertyFormsService.java | 6 ++--
.../SecurityDescriptionFormsService.java | 2 +-
.../SecurityEventCashFlowFormsService.java | 8 ++---
.../service/SecurityQuoteFormsService.java | 4 +--
.../service/SecurityRepositoryHelper.java | 36 +++++++++----------
.../service/TransactionFormsService.java | 22 ++++++------
13 files changed, 63 insertions(+), 63 deletions(-)
diff --git a/src/main/java/ru/investbook/api/AbstractRestController.java b/src/main/java/ru/investbook/api/AbstractRestController.java
index ed7f0724..e30c5409 100644
--- a/src/main/java/ru/investbook/api/AbstractRestController.java
+++ b/src/main/java/ru/investbook/api/AbstractRestController.java
@@ -76,7 +76,7 @@ protected ResponseEntity post(Pojo object) {
try {
ID id = getId(object);
if (id == null) {
- return createOrUpdateEntity(object);
+ return createOrUpdateEntityAndReturnCreateStatus(object);
}
if (existsById(id)) {
return ResponseEntity
@@ -87,7 +87,7 @@ protected ResponseEntity post(Pojo object) {
// Если убрать @Transactional над методом, то следующая строка может обновить объект по ошибке.
// Это возможно, если объект был создан другим потоком после проверки существования строки по ID.
// По этой причине @Transactional убирать не нужно.
- return createOrUpdateEntity(object);
+ return createOrUpdateEntityAndReturnCreateStatus(object);
}
} catch (Exception e) {
throw new InternalServerErrorException("Не могу создать объект", e);
@@ -117,10 +117,10 @@ public ResponseEntity put(ID id, Pojo object) {
"запроса [" + getId(object) + "] не совпадают");
}
if (existsById(id)) {
- saveAndFlush(object);
+ createOrUpdateEntity(object);
return ResponseEntity.ok().build();
} else {
- return createOrUpdateEntity(object);
+ return createOrUpdateEntityAndReturnCreateStatus(object);
}
} catch (Exception e) {
throw new InternalServerErrorException("Не могу создать объект", e);
@@ -133,16 +133,16 @@ private boolean existsById(ID id) {
protected abstract Pojo updateId(ID id, Pojo object);
- private Entity saveAndFlush(Pojo object) {
+ private Entity createOrUpdateEntity(Pojo object) {
Entity entity = converter.toEntity(object);
- return repository.saveAndFlush(entity);
+ return repository.save(entity);
}
/**
* @return response entity with http CREATE status, Location http header and body
*/
- private ResponseEntity createOrUpdateEntity(Pojo object) throws URISyntaxException {
- Entity entity = saveAndFlush(object);
+ private ResponseEntity createOrUpdateEntityAndReturnCreateStatus(Pojo object) throws URISyntaxException {
+ Entity entity = createOrUpdateEntity(object);
Pojo savedObject = converter.fromEntity(entity);
URI locationURI = getLocationURI(savedObject);
return ResponseEntity
diff --git a/src/main/java/ru/investbook/parser/SecurityRegistrarImpl.java b/src/main/java/ru/investbook/parser/SecurityRegistrarImpl.java
index 7743a4bf..118fa623 100644
--- a/src/main/java/ru/investbook/parser/SecurityRegistrarImpl.java
+++ b/src/main/java/ru/investbook/parser/SecurityRegistrarImpl.java
@@ -127,7 +127,7 @@ private int declareSecurityByIsin(String isin, SecurityType defaultType, Supplie
return repository.findByIsin(isin)
.or(() -> Optional.of(supplier.get())
.map(builder -> buildSecurity(builder, defaultType))
- .map(security -> saveAndFlush(security, () -> repository.findByIsin(isin))))
+ .map(security -> save(security, () -> repository.findByIsin(isin))))
.map(SecurityEntity::getId)
.orElseThrow(() -> new RuntimeException("Не смог сохранить ЦБ с ISIN = " + isin));
}
@@ -136,7 +136,7 @@ private Integer declareSecurityByName(String name, SecurityType defaultType, Sup
return repository.findByName(name)
.or(() -> Optional.of(supplier.get())
.map(builder -> buildSecurity(builder, defaultType))
- .map(security -> saveAndFlush(security, () -> repository.findByName(name))))
+ .map(security -> save(security, () -> repository.findByName(name))))
.map(SecurityEntity::getId)
.orElseThrow(() -> new RuntimeException("Не смог сохранить актив с наименованием = " + name));
}
@@ -145,7 +145,7 @@ private Integer declareSecurityByTicker(String ticker, SecurityType defaultType,
return repository.findByTicker(ticker)
.or(() -> Optional.of(supplier.get())
.map(builder -> buildSecurity(builder, defaultType))
- .map(security -> saveAndFlush(security, () -> repository.findByTicker(ticker))))
+ .map(security -> save(security, () -> repository.findByTicker(ticker))))
.map(SecurityEntity::getId)
.orElseThrow(() -> new RuntimeException("Не смог сохранить актив с тикером = " + ticker));
}
@@ -153,7 +153,7 @@ private Integer declareSecurityByTicker(String ticker, SecurityType defaultType,
private Integer declareContractByTicker(String contract, SecurityType contractType) {
return repository.findByTicker(contract)
.or(() -> Optional.of(Security.builder().ticker(contract).type(contractType).build())
- .map(security -> saveAndFlush(security, () -> repository.findByTicker(contract))))
+ .map(security -> save(security, () -> repository.findByTicker(contract))))
.map(SecurityEntity::getId)
.orElseThrow(() -> new RuntimeException("Не смог сохранить контракт = " + contract));
}
@@ -166,10 +166,10 @@ private Security buildSecurity(SecurityBuilder builder, SecurityType defaultType
return security;
}
- private SecurityEntity saveAndFlush(Security security, Supplier> supplier) {
+ private SecurityEntity save(Security security, Supplier> supplier) {
try {
validator.validate(security);
- return repository.saveAndFlush(converter.toEntity(security));
+ return repository.save(converter.toEntity(security));
} catch (ConstraintViolationException e) {
throw new RuntimeException("Не смог сохранить ценную бумагу в БД: " + security + ", " + e.getMessage());
} catch (Exception e) {
diff --git a/src/main/java/ru/investbook/parser/investbook/AbstractSecurityAwareInvestbookTable.java b/src/main/java/ru/investbook/parser/investbook/AbstractSecurityAwareInvestbookTable.java
index d53ca61a..583c6b0a 100644
--- a/src/main/java/ru/investbook/parser/investbook/AbstractSecurityAwareInvestbookTable.java
+++ b/src/main/java/ru/investbook/parser/investbook/AbstractSecurityAwareInvestbookTable.java
@@ -86,7 +86,7 @@ private SecurityEntity createStockOrBond(String securityTickerNameOrIsin, Securi
} else {
builder.name(securityTickerNameOrIsin);
}
- return securityRepository.saveAndFlush(securityConverter.toEntity(builder.build()));
+ return securityRepository.save(securityConverter.toEntity(builder.build()));
}
protected String getTradeId(String portfolio, int securityId, Instant instant) {
diff --git a/src/main/java/ru/investbook/parser/investbook/InvestbookSecurityDepositAndWithdrawalTable.java b/src/main/java/ru/investbook/parser/investbook/InvestbookSecurityDepositAndWithdrawalTable.java
index 61fa39c8..b49700d1 100644
--- a/src/main/java/ru/investbook/parser/investbook/InvestbookSecurityDepositAndWithdrawalTable.java
+++ b/src/main/java/ru/investbook/parser/investbook/InvestbookSecurityDepositAndWithdrawalTable.java
@@ -78,6 +78,6 @@ private SecurityEntity createStockOrBond(String securityTickerNameOrIsin) {
.type(SecurityType.STOCK_OR_BOND)
.name(securityTickerNameOrIsin)
.build();
- return securityRepository.saveAndFlush(securityConverter.toEntity(security));
+ return securityRepository.save(securityConverter.toEntity(security));
}
}
diff --git a/src/main/java/ru/investbook/web/forms/service/EventCashFlowFormsService.java b/src/main/java/ru/investbook/web/forms/service/EventCashFlowFormsService.java
index e46ab0e3..86ad137f 100644
--- a/src/main/java/ru/investbook/web/forms/service/EventCashFlowFormsService.java
+++ b/src/main/java/ru/investbook/web/forms/service/EventCashFlowFormsService.java
@@ -81,7 +81,7 @@ public Page getPage(EventCashFlowFormFilterModel filter) {
@Transactional
public void save(EventCashFlowModel e) {
- saveAndFlush(e.getPortfolio());
+ savePortfolio(e.getPortfolio());
if (e.isAttachedToSecurity()) {
saveSecurityEventCashFlow(e);
} else {
@@ -90,7 +90,7 @@ public void save(EventCashFlowModel e) {
}
private void saveSecurityEventCashFlow(EventCashFlowModel e) {
- int savedSecurityId = securityRepositoryHelper.saveAndFlushSecurity(requireNonNull(e.getAttachedSecurity()));
+ int savedSecurityId = securityRepositoryHelper.saveSecurity(requireNonNull(e.getAttachedSecurity()));
SecurityEventCashFlowEntity entity = securityEventCashFlowRepository.save(
securityEventCashFlowConverter.toEntity(SecurityEventCashFlow.builder()
// no id(), it is always the new object
@@ -123,9 +123,9 @@ private void saveEventCashFlow(EventCashFlowModel e) {
eventCashFlowRepository.flush();
}
- private void saveAndFlush(String portfolio) {
+ private void savePortfolio(String portfolio) {
if (!portfolioRepository.existsById(portfolio)) {
- portfolioRepository.saveAndFlush(
+ portfolioRepository.save(
portfolioConverter.toEntity(Portfolio.builder()
.id(portfolio)
.build()));
diff --git a/src/main/java/ru/investbook/web/forms/service/ForeignExchangeRateFormsService.java b/src/main/java/ru/investbook/web/forms/service/ForeignExchangeRateFormsService.java
index 95a8a633..ac8ef907 100644
--- a/src/main/java/ru/investbook/web/forms/service/ForeignExchangeRateFormsService.java
+++ b/src/main/java/ru/investbook/web/forms/service/ForeignExchangeRateFormsService.java
@@ -69,7 +69,7 @@ public Page getPage(ForeignExchangeRateFormFilterModel
@Transactional
public void save(ForeignExchangeRateModel m) {
- foreignExchangeRateRepository.saveAndFlush(
+ foreignExchangeRateRepository.save(
foreignExchangeRateConverter.toEntity(ForeignExchangeRate.builder()
.date(m.getDate())
.currencyPair((m.getBaseCurrency() + m.getQuoteCurrency()).toUpperCase())
diff --git a/src/main/java/ru/investbook/web/forms/service/PortfolioCashFormsService.java b/src/main/java/ru/investbook/web/forms/service/PortfolioCashFormsService.java
index 96e26c34..a5ddeac8 100644
--- a/src/main/java/ru/investbook/web/forms/service/PortfolioCashFormsService.java
+++ b/src/main/java/ru/investbook/web/forms/service/PortfolioCashFormsService.java
@@ -75,7 +75,7 @@ public Page getPage(PortfolioCashFormFilterModel filter) {
@Transactional
public void save(PortfolioCashModel m) {
- saveAndFlush(m.getPortfolio());
+ savePortfolio(m.getPortfolio());
PortfolioCash cash = PortfolioCash.builder()
.id(m.getId())
.portfolio(m.getPortfolio())
@@ -91,9 +91,9 @@ public void save(PortfolioCashModel m) {
portfolioCashRepository.flush();
}
- private void saveAndFlush(String portfolio) {
+ private void savePortfolio(String portfolio) {
if (!portfolioRepository.existsById(portfolio)) {
- portfolioRepository.saveAndFlush(
+ portfolioRepository.save(
portfolioConverter.toEntity(Portfolio.builder()
.id(portfolio)
.build()));
diff --git a/src/main/java/ru/investbook/web/forms/service/PortfolioPropertyFormsService.java b/src/main/java/ru/investbook/web/forms/service/PortfolioPropertyFormsService.java
index ce2394f9..7e76089d 100644
--- a/src/main/java/ru/investbook/web/forms/service/PortfolioPropertyFormsService.java
+++ b/src/main/java/ru/investbook/web/forms/service/PortfolioPropertyFormsService.java
@@ -81,7 +81,7 @@ public Page getPage(PortfolioPropertyFormFilterModel fil
@Transactional
public void save(PortfolioPropertyModel m) {
- saveAndFlush(m.getPortfolio());
+ savePortfolio(m.getPortfolio());
PortfolioProperty.PortfolioPropertyBuilder builder = PortfolioProperty.builder()
.id(m.getId())
.portfolio(m.getPortfolio())
@@ -100,9 +100,9 @@ public void save(PortfolioPropertyModel m) {
portfolioPropertyRepository.flush();
}
- private void saveAndFlush(String portfolio) {
+ private void savePortfolio(String portfolio) {
if (!portfolioRepository.existsById(portfolio)) {
- portfolioRepository.saveAndFlush(
+ portfolioRepository.save(
portfolioConverter.toEntity(Portfolio.builder()
.id(portfolio)
.build()));
diff --git a/src/main/java/ru/investbook/web/forms/service/SecurityDescriptionFormsService.java b/src/main/java/ru/investbook/web/forms/service/SecurityDescriptionFormsService.java
index fbbfc768..867b1638 100644
--- a/src/main/java/ru/investbook/web/forms/service/SecurityDescriptionFormsService.java
+++ b/src/main/java/ru/investbook/web/forms/service/SecurityDescriptionFormsService.java
@@ -67,7 +67,7 @@ public Page getPage(SecurityDescriptionFormFilterModel
@Transactional
public void save(SecurityDescriptionModel m) {
- int savedSecurityId = securityRepositoryHelper.saveAndFlushSecurity(m);
+ int savedSecurityId = securityRepositoryHelper.saveSecurity(m);
securityDescriptionRepository.createOrUpdateSector(savedSecurityId, m.getSector());
}
diff --git a/src/main/java/ru/investbook/web/forms/service/SecurityEventCashFlowFormsService.java b/src/main/java/ru/investbook/web/forms/service/SecurityEventCashFlowFormsService.java
index 5909bcf1..10595226 100644
--- a/src/main/java/ru/investbook/web/forms/service/SecurityEventCashFlowFormsService.java
+++ b/src/main/java/ru/investbook/web/forms/service/SecurityEventCashFlowFormsService.java
@@ -79,8 +79,8 @@ public Page getPage(SecurityEventCashFlowFormFilterM
@Transactional
public void save(SecurityEventCashFlowModel e) {
- saveAndFlush(e.getPortfolio());
- int savedSecurityId = securityRepositoryHelper.saveAndFlushSecurity(e);
+ savePortfolio(e.getPortfolio());
+ int savedSecurityId = securityRepositoryHelper.saveSecurity(e);
SecurityEventCashFlowBuilder builder = SecurityEventCashFlow.builder()
.portfolio(e.getPortfolio())
.timestamp(e.getDate().atTime(e.getTime()).atZone(zoneId).toInstant())
@@ -109,9 +109,9 @@ public void save(SecurityEventCashFlowModel e) {
securityEventCashFlowRepository.flush();
}
- private void saveAndFlush(String portfolio) {
+ private void savePortfolio(String portfolio) {
if (!portfolioRepository.existsById(portfolio)) {
- portfolioRepository.saveAndFlush(
+ portfolioRepository.save(
portfolioConverter.toEntity(Portfolio.builder()
.id(portfolio)
.build()));
diff --git a/src/main/java/ru/investbook/web/forms/service/SecurityQuoteFormsService.java b/src/main/java/ru/investbook/web/forms/service/SecurityQuoteFormsService.java
index a69effe9..11b33b0d 100644
--- a/src/main/java/ru/investbook/web/forms/service/SecurityQuoteFormsService.java
+++ b/src/main/java/ru/investbook/web/forms/service/SecurityQuoteFormsService.java
@@ -69,8 +69,8 @@ public Page getPage(SecurityQuoteFormFilterModel filter) {
@Transactional
public void save(SecurityQuoteModel e) {
- int savedSecurityId = securityRepositoryHelper.saveAndFlushSecurity(e);
- SecurityQuoteEntity entity = securityQuoteRepository.saveAndFlush(
+ int savedSecurityId = securityRepositoryHelper.saveSecurity(e);
+ SecurityQuoteEntity entity = securityQuoteRepository.save(
securityQuoteConverter.toEntity(SecurityQuote.builder()
.id(e.getId())
.security(savedSecurityId)
diff --git a/src/main/java/ru/investbook/web/forms/service/SecurityRepositoryHelper.java b/src/main/java/ru/investbook/web/forms/service/SecurityRepositoryHelper.java
index 44b33b14..87c430d8 100644
--- a/src/main/java/ru/investbook/web/forms/service/SecurityRepositoryHelper.java
+++ b/src/main/java/ru/investbook/web/forms/service/SecurityRepositoryHelper.java
@@ -52,8 +52,8 @@ public class SecurityRepositoryHelper {
* Generate securityId if needed, save security to DB, update securityId for model if needed
* @return saved security id
*/
- public int saveAndFlushSecurity(SecurityDescriptionModel m) {
- int savedSecurityId = saveAndFlush(m.getSecurityId(), m.getSecurityIsin(), m.getSecurityName(), m.getSecurityType());
+ public int saveSecurity(SecurityDescriptionModel m) {
+ int savedSecurityId = saveSecurity(m.getSecurityId(), m.getSecurityIsin(), m.getSecurityName(), m.getSecurityType());
m.setSecurityId(savedSecurityId);
return savedSecurityId;
}
@@ -62,62 +62,62 @@ public int saveAndFlushSecurity(SecurityDescriptionModel m) {
* Generate securityId if needed, save security to DB, update securityId for model if needed
* @return saved security id
*/
- public int saveAndFlushSecurity(EventCashFlowModel.AttachedSecurity s) {
+ public int saveSecurity(EventCashFlowModel.AttachedSecurity s) {
// EventCashFlowModel. AttachToSecurity может содержать только SHARE или BOND.
// Тип SHARE захардкожен, тип может быть ошибочен, но для текущего алгоритма сохранения ЦБ этого типа достаточно
- return saveAndFlush(s.getSecurityIsin(), s.getSecurityName(), SecurityType.SHARE);
+ return saveSecurity(s.getSecurityIsin(), s.getSecurityName(), SecurityType.SHARE);
}
/**
* Generate securityId if needed, save security to DB, update securityId for model if needed
* @return saved security id
*/
- public int saveAndFlushSecurity(SecurityEventCashFlowModel m) {
- return saveAndFlush(m.getSecurityIsin(), m.getSecurityName(), m.getSecurityType());
+ public int saveSecurity(SecurityEventCashFlowModel m) {
+ return saveSecurity(m.getSecurityIsin(), m.getSecurityName(), m.getSecurityType());
}
/**
* Generate securityId if needed, save security to DB, update securityId for model if needed
* @return saved security id
*/
- public int saveAndFlushSecurity(SecurityQuoteModel m) {
- return saveAndFlush(m.getSecurityIsin(), m.getSecurityName(), m.getSecurityType());
+ public int saveSecurity(SecurityQuoteModel m) {
+ return saveSecurity(m.getSecurityIsin(), m.getSecurityName(), m.getSecurityType());
}
/**
* Generate securityId if needed, save security to DB, update securityId for model if needed
* @return saved security id
*/
- public int saveAndFlushSecurity(TransactionModel m) {
- return saveAndFlush(m.getSecurityIsin(), m.getSecurityName(), m.getSecurityType());
+ public int saveSecurity(TransactionModel m) {
+ return saveSecurity(m.getSecurityIsin(), m.getSecurityName(), m.getSecurityType());
}
/**
* Generate securityId if needed, save security to DB, update securityId for model if needed
* @return saved security id
*/
- public int saveAndFlushSecurity(SplitModel m) {
- return saveAndFlush(m.getSecurityIsin(), m.getSecurityName(), SecurityType.SHARE);
+ public int saveSecurity(SplitModel m) {
+ return saveSecurity(m.getSecurityIsin(), m.getSecurityName(), SecurityType.SHARE);
}
/**
* @return securityId from DB
*/
- private int saveAndFlush(Integer securityId, String isin, String securityName, SecurityType securityType) {
+ private int saveSecurity(Integer securityId, String isin, String securityName, SecurityType securityType) {
SecurityEntity security = ofNullable(securityId)
.flatMap(securityRepository::findById)
.or(() -> findSecurity(isin, securityName, securityType))
.orElseGet(SecurityEntity::new);
- return saveAndFlush(security, isin, securityName, securityType, true);
+ return saveSecurity(security, isin, securityName, securityType, true);
}
- private int saveAndFlush(String isin, String securityName, SecurityType securityType) {
+ private int saveSecurity(String isin, String securityName, SecurityType securityType) {
SecurityEntity security = findSecurity(isin, securityName, securityType)
.orElseGet(SecurityEntity::new);
- return saveAndFlush(security, isin, securityName, securityType, false);
+ return saveSecurity(security, isin, securityName, securityType, false);
}
- private int saveAndFlush(SecurityEntity security, String isin, String securityName,
+ private int saveSecurity(SecurityEntity security, String isin, String securityName,
SecurityType securityType, boolean forceRewriteByEmptyIsin) {
isin = validateIsin(isin);
if (isin == null && !forceRewriteByEmptyIsin) {
@@ -140,7 +140,7 @@ private int saveAndFlush(SecurityEntity security, String isin, String securityNa
security.setName(securityName);
}
}
- security = securityRepository.saveAndFlush(security);
+ security = securityRepository.save(security);
return security.getId();
}
diff --git a/src/main/java/ru/investbook/web/forms/service/TransactionFormsService.java b/src/main/java/ru/investbook/web/forms/service/TransactionFormsService.java
index 34c2f7ab..986fbd13 100644
--- a/src/main/java/ru/investbook/web/forms/service/TransactionFormsService.java
+++ b/src/main/java/ru/investbook/web/forms/service/TransactionFormsService.java
@@ -122,7 +122,7 @@ private Page getTransactionModels(Specification
Date: Mon, 27 May 2024 00:25:19 +0300
Subject: [PATCH 19/40] update spring-boot to 3.3.0
---
README-en.md | 2 +-
README.md | 2 +-
pom.xml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/README-en.md b/README-en.md
index 86373d45..f45d38c8 100644
--- a/README-en.md
+++ b/README-en.md
@@ -2,7 +2,7 @@
[](README.md)
[![java-version](https://img.shields.io/badge/java-22-brightgreen?style=flat-square)](https://openjdk.org/)
-[![spring-boot-version](https://img.shields.io/badge/spring--boot-3.2.5-brightgreen?style=flat-square)](https://github.com/spring-projects/spring-boot/releases)
+[![spring-boot-version](https://img.shields.io/badge/spring--boot-3.3.0-brightgreen?style=flat-square)](https://github.com/spring-projects/spring-boot/releases)
[![hits-of-code](https://img.shields.io/badge/dynamic/json?style=flat-square&color=lightblue&label=hits-of-code&url=https://hitsofcode.com/github/spacious-team/investbook/json?branch=develop&query=$.count)](https://hitsofcode.com/github/spacious-team/investbook/view?branch=develop)
[![github-closed-pull-requests](https://img.shields.io/github/issues-pr-closed/spacious-team/investbook?style=flat-square&color=brightgreen)](https://github.com/spacious-team/investbook/pulls?q=is%3Apr+is%3Aclosed)
[![github-workflow-status](https://img.shields.io/github/actions/workflow/status/spacious-team/investbook/publish-docker.yml?style=flat-square&branch=master)](https://github.com/spacious-team/investbook/actions/workflows/publish-docker.yml)
diff --git a/README.md b/README.md
index 915948f9..22df5b2a 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](README.md)
[![java-version](https://img.shields.io/badge/java-22-brightgreen?style=flat-square)](https://openjdk.org/)
-[![spring-boot-version](https://img.shields.io/badge/spring--boot-3.2.5-brightgreen?style=flat-square)](https://github.com/spring-projects/spring-boot/releases)
+[![spring-boot-version](https://img.shields.io/badge/spring--boot-3.3.0-brightgreen?style=flat-square)](https://github.com/spring-projects/spring-boot/releases)
[![hits-of-code](https://img.shields.io/badge/dynamic/json?style=flat-square&color=lightblue&label=hits-of-code&url=https://hitsofcode.com/github/spacious-team/investbook/json?branch=develop&query=$.count)](https://hitsofcode.com/github/spacious-team/investbook/view?branch=develop)
[![github-closed-pull-requests](https://img.shields.io/github/issues-pr-closed/spacious-team/investbook?style=flat-square&color=brightgreen)](https://github.com/spacious-team/investbook/pulls?q=is%3Apr+is%3Aclosed)
[![github-workflow-status](https://img.shields.io/github/actions/workflow/status/spacious-team/investbook/publish-docker.yml?style=flat-square&branch=master)](https://github.com/spacious-team/investbook/actions/workflows/publish-docker.yml)
diff --git a/pom.xml b/pom.xml
index 748f2853..514161a3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,7 +23,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.2.5
+ 3.3.0
ru.investbook
From 333ab29881658a738288134adb34a1fe258971e3 Mon Sep 17 00:00:00 2001
From: inav975
Date: Wed, 5 Jun 2024 01:15:58 +0300
Subject: [PATCH 20/40] Update README-en.md
---
README-en.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README-en.md b/README-en.md
index f45d38c8..c6ce3813 100644
--- a/README-en.md
+++ b/README-en.md
@@ -62,7 +62,7 @@ For each account separately and summing up a single total for all accounts, the
- [portfolio](src/main/asciidoc/portfolio-status.adoc) of securities with information about the current position,
- average price purchases and yield of securities (CHISTVNDOH/XIRR), taking into account hedging positions in the derivatives
+ average price purchases and yield of securities (XIRR), taking into account hedging positions in the derivatives
market and the average purchase price of currency;
![portfolio](https://user-images.githubusercontent.com/11336712/104820094-af2dce80-5843-11eb-8083-6521ea537334.png)
- share of a security in a [portfolio](src/main/asciidoc/portfolio-status.adoc);
@@ -85,7 +85,7 @@ For each account separately and summing up a single total for all accounts, the
![foreign-market](https://user-images.githubusercontent.com/11336712/84881751-fa59e600-b096-11ea-8b83-19d1c1229d73.png)
- [input and output](src/main/asciidoc/securities-deposit-and-withdrawal.adoc) of securities from/to other accounts, conversion, split of shares (AAPL, TSLA, etc.);
-- [profitability](src/main/asciidoc/cash-flow.adoc) of portfolio (CHISTVNDOH/XIRR), replenishments, write-offs, transfers from/to other accounts,
+- [profitability](src/main/asciidoc/cash-flow.adoc) of portfolio (XIRR), replenishments, write-offs, transfers from/to other accounts,
- current cash balance;
![cash-in](https://user-images.githubusercontent.com/11336712/100395491-3172f100-3052-11eb-9652-cd5730ac2e6f.png)
- [tax](src/main/asciidoc/tax.adoc) burden, including the
From 89db16a2db83877eb65e45b647dcea46c920a618 Mon Sep 17 00:00:00 2001
From: rikottafreska
Date: Wed, 12 Jun 2024 11:11:34 +0300
Subject: [PATCH 21/40] Update AssignedOrIdentityGenerator.java
---
.../java/ru/investbook/entity/AssignedOrIdentityGenerator.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/ru/investbook/entity/AssignedOrIdentityGenerator.java b/src/main/java/ru/investbook/entity/AssignedOrIdentityGenerator.java
index b3b14b45..46004081 100644
--- a/src/main/java/ru/investbook/entity/AssignedOrIdentityGenerator.java
+++ b/src/main/java/ru/investbook/entity/AssignedOrIdentityGenerator.java
@@ -24,7 +24,7 @@
/**
* If Entity ID is not null, then this ID is stored to DB.
- * If Entity ID is null, then RDBM should generate ID itself.
+ * If Entity ID is null, then RDBMS should generate ID itself.
*
* In other words: this generator behaves like {@link org.hibernate.id.Assigned} generator if entity's field,
* marked by {@link Id} annotation, is not null, or like {@link org.hibernate.id.IdentityGenerator} if this field is null.
From b31a9a1bc3adf326c80eb597a5e81b933f60b15a Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sat, 20 Jul 2024 15:34:06 +0300
Subject: [PATCH 22/40] add EntityRepositoryService
---
.../api/AbstractEntityRepositoryService.java | 151 ++++++++++++++++++
.../api/AbstractRestController.java | 87 ++++------
.../api/EntityRepositoryService.java | 78 +++++++++
.../api/EventCashFlowRestController.java | 7 -
.../ForeignExchangeRateRestController.java | 19 +--
.../investbook/api/IssuerRestController.java | 7 -
.../api/PortfolioCashRestController.java | 7 -
.../api/PortfolioPropertyRestController.java | 7 -
.../api/PortfolioRestController.java | 7 -
.../SecurityDescriptionRestController.java | 7 -
.../SecurityEventCashFlowRestController.java | 7 -
.../api/SecurityQuoteRestController.java | 7 -
.../api/SecurityRestController.java | 7 -
.../TransactionCashFlowRestController.java | 6 -
.../api/TransactionRestController.java | 6 -
.../parser/InvestbookApiClient.java | 2 +
16 files changed, 268 insertions(+), 144 deletions(-)
create mode 100644 src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java
create mode 100644 src/main/java/ru/investbook/api/EntityRepositoryService.java
diff --git a/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java b/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java
new file mode 100644
index 00000000..6541e33a
--- /dev/null
+++ b/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java
@@ -0,0 +1,151 @@
+/*
+ * InvestBook
+ * Copyright (C) 2024 Spacious Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package ru.investbook.api;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.hibernate.Session;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionTemplate;
+import ru.investbook.converter.EntityConverter;
+
+import java.util.Optional;
+
+import static org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW;
+import static ru.investbook.repository.RepositoryHelper.isUniqIndexViolationException;
+
+@Slf4j
+@RequiredArgsConstructor
+public abstract class AbstractEntityRepositoryService implements EntityRepositoryService {
+ private final JpaRepository repository;
+ private final EntityConverter converter;
+ @Autowired
+ private EntityManager entityManager;
+ @Autowired
+ private PlatformTransactionManager transactionManager;
+ private TransactionTemplate transactionTemplateRequired;
+ private TransactionTemplate transactionTemplateRequiresNew;
+
+ @PostConstruct
+ void init() {
+ transactionTemplateRequired = new TransactionTemplate(transactionManager);
+ transactionTemplateRequiresNew = new TransactionTemplate(transactionManager);
+ transactionTemplateRequiresNew.setPropagationBehavior(PROPAGATION_REQUIRES_NEW);
+ }
+
+ @Override
+ public boolean existsById(ID id) {
+ return repository.existsById(id);
+ }
+
+ @Override
+ public Optional getById(ID id) {
+ return repository.findById(id)
+ .map(converter::fromEntity);
+ }
+
+ @Override
+ public Page get(Pageable pageable) {
+ return repository.findAll(pageable)
+ .map(converter::fromEntity);
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public boolean insert(Pojo object) {
+ if (entityManager instanceof Session hibernateSpecificSession) {
+ try {
+ Entity entity = converter.toEntity(object);
+ transactionTemplateRequiresNew.executeWithoutResult(_ -> hibernateSpecificSession.save(entity));
+ return true;
+ } catch (Exception e) {
+ if (isUniqIndexViolationException(e)) {
+ return false; // can't insert, object with same ID already exists in DB or unique key constraint violation
+ }
+ log.error("Can't INSERT by optimized deprecated Hibernate method save(): {}", object, e);
+ }
+ }
+ Boolean result = transactionTemplateRequired.execute(_ -> create(object));
+ return Boolean.TRUE.equals(result);
+ }
+
+ @Override
+ @Transactional
+ public boolean create(Pojo object) {
+ return createInternal(object)
+ .isPresent();
+ }
+
+ @Override
+ @Transactional
+ public Optional createAndGet(Pojo object) {
+ return createInternal(object)
+ .map(converter::fromEntity);
+ }
+
+ /**
+ * Creates a new object (with SELECT check)
+ *
+ * @return created entity if object is created or empty Optional otherwise
+ * @implSpec Should be called in transaction
+ */
+ private Optional createInternal(Pojo object) {
+ ID id = getId(object);
+ if (id != null && existsById(id)) {
+ return Optional.empty();
+ }
+ // Если работать не в транзакции, то следующая строка может повторно создать объект.
+ // Это возможно, если объект был создан другим потоком после проверки существования строки по ID.
+ // Метод должен работать в транзакции.
+ Entity savedEntity = createOrUpdateInternal(object);
+ return Optional.of(savedEntity);
+ }
+
+ @Override
+ @Transactional
+ public void createOrUpdate(Pojo object) {
+ createOrUpdateInternal(object);
+ }
+
+ @Override
+ @Transactional
+ public Pojo createOrUpdateAndGet(Pojo object) {
+ Entity savedEntity = createOrUpdateInternal(object);
+ return converter.fromEntity(savedEntity);
+ }
+
+ private Entity createOrUpdateInternal(Pojo object) {
+ Entity entity = converter.toEntity(object);
+ return repository.save(entity);
+ }
+
+ @Override
+ public void deleteById(ID id) {
+ repository.deleteById(id);
+ }
+
+ protected abstract ID getId(Pojo object);
+}
diff --git a/src/main/java/ru/investbook/api/AbstractRestController.java b/src/main/java/ru/investbook/api/AbstractRestController.java
index e30c5409..a70b58b8 100644
--- a/src/main/java/ru/investbook/api/AbstractRestController.java
+++ b/src/main/java/ru/investbook/api/AbstractRestController.java
@@ -19,9 +19,7 @@
package ru.investbook.api;
import jakarta.persistence.GeneratedValue;
-import lombok.RequiredArgsConstructor;
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.Pageable;
+import lombok.SneakyThrows;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -30,38 +28,28 @@
import ru.investbook.converter.EntityConverter;
import java.net.URI;
-import java.net.URISyntaxException;
import java.util.Objects;
-import java.util.Optional;
import static java.nio.charset.StandardCharsets.UTF_8;
-@RequiredArgsConstructor
-public abstract class AbstractRestController {
- protected final JpaRepository repository;
- protected final EntityConverter converter;
+public abstract class AbstractRestController extends AbstractEntityRepositoryService {
-
- protected Page get(Pageable pageable) {
- return repository.findAll(pageable)
- .map(converter::fromEntity);
+ protected AbstractRestController(JpaRepository repository, EntityConverter converter) {
+ super(repository, converter);
}
/**
- * Get the entity.
+ * Gets the entity.
* If entity not exists NOT_FOUND http status will be returned.
*/
public ResponseEntity get(ID id) {
return getById(id)
- .map(converter::fromEntity)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
- protected abstract Optional getById(ID id);
-
/**
- * Create a new entity.
+ * Creates a new entity.
* If entity has ID and record with this ID already exists in DB, CONFLICT http status and Location header was returned.
* Otherwise, CREATE http status will be returned with Location header.
* When creating new object, ID may be passed, but that ID only used when no {@link GeneratedValue} set
@@ -74,30 +62,19 @@ public ResponseEntity get(ID id) {
@Transactional
protected ResponseEntity post(Pojo object) {
try {
- ID id = getId(object);
- if (id == null) {
- return createOrUpdateEntityAndReturnCreateStatus(object);
- }
- if (existsById(id)) {
- return ResponseEntity
- .status(HttpStatus.CONFLICT)
- .location(getLocationURI(object))
- .build();
- } else {
- // Если убрать @Transactional над методом, то следующая строка может обновить объект по ошибке.
- // Это возможно, если объект был создан другим потоком после проверки существования строки по ID.
- // По этой причине @Transactional убирать не нужно.
- return createOrUpdateEntityAndReturnCreateStatus(object);
- }
+ return createAndGet(object)
+ .map(this::createResponseWithLocationHeader)
+ .orElseGet(() -> ResponseEntity
+ .status(HttpStatus.CONFLICT)
+ .location(getLocationURI(object))
+ .build());
} catch (Exception e) {
throw new InternalServerErrorException("Не могу создать объект", e);
}
}
- protected abstract ID getId(Pojo object);
-
/**
- * Update or create a new entity.
+ * Updates or creates a new entity.
* In update case method returns OK http status.
* In create case method returns CREATE http status and Location header.
* When creating new object, ID may be passed in the object, but it should be same as {@code id} argument.
@@ -116,50 +93,44 @@ public ResponseEntity put(ID id, Pojo object) {
throw new BadRequestException("Идентификатор объекта, переданный в URI [" + id + "] и в теле " +
"запроса [" + getId(object) + "] не совпадают");
}
- if (existsById(id)) {
- createOrUpdateEntity(object);
- return ResponseEntity.ok().build();
+ if (create(object)) {
+ return ResponseEntity.ok().build(); // todo no content
} else {
- return createOrUpdateEntityAndReturnCreateStatus(object);
+ Pojo savedObject = createOrUpdateAndGet(object);
+ return createResponseWithLocationHeader(savedObject); // todo change http status?
}
} catch (Exception e) {
throw new InternalServerErrorException("Не могу создать объект", e);
}
}
- private boolean existsById(ID id) {
- return repository.existsById(id);
- }
-
- protected abstract Pojo updateId(ID id, Pojo object);
-
- private Entity createOrUpdateEntity(Pojo object) {
- Entity entity = converter.toEntity(object);
- return repository.save(entity);
+ /**
+ * Deletes object from storage. Always return OK http status with empty body.
+ */
+ // TODO should impl 404 status? https://stackoverflow.com/questions/4088350/is-rest-delete-really-idempotent
+ public void delete(ID id) {
+ deleteById(id);
}
/**
* @return response entity with http CREATE status, Location http header and body
*/
- private ResponseEntity createOrUpdateEntityAndReturnCreateStatus(Pojo object) throws URISyntaxException {
- Entity entity = createOrUpdateEntity(object);
- Pojo savedObject = converter.fromEntity(entity);
- URI locationURI = getLocationURI(savedObject);
+ private ResponseEntity createResponseWithLocationHeader(Pojo object) {
+ URI locationURI = getLocationURI(object);
return ResponseEntity
.created(locationURI)
.build();
}
- protected URI getLocationURI(Pojo object) throws URISyntaxException {
+ @SneakyThrows
+ protected URI getLocationURI(Pojo object) {
return new URI(UriUtils.encodePath(getLocation() + "/" + getId(object), UTF_8));
}
protected abstract String getLocation();
/**
- * Delete object from storage. Always return OK http status with empty body.
+ * Returns new object with updated ID
*/
- public void delete(ID id) {
- repository.deleteById(id);
- }
+ protected abstract Pojo updateId(ID id, Pojo object);
}
diff --git a/src/main/java/ru/investbook/api/EntityRepositoryService.java b/src/main/java/ru/investbook/api/EntityRepositoryService.java
new file mode 100644
index 00000000..0a9e46c8
--- /dev/null
+++ b/src/main/java/ru/investbook/api/EntityRepositoryService.java
@@ -0,0 +1,78 @@
+/*
+ * InvestBook
+ * Copyright (C) 2024 Spacious Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package ru.investbook.api;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.util.Optional;
+
+public interface EntityRepositoryService {
+
+ boolean existsById(ID id);
+
+ Optional getById(ID id);
+
+ Page get(Pageable pageable);
+
+ /**
+ * Creates a new object with direct INSERT into DB (without prior SELECT call) if possible,
+ * calls {@link #create(Object)} otherwise.
+ *
+ * @return true if object is created or false otherwise
+ * @implNote Method performance is the same as {@link #create(Object)} for H2 2.2.224 and MariaDB 11.2
+ */
+ boolean insert(Pojo object);
+
+ /**
+ * Creates new object, doesn't update.
+ * Calls SELECT to check if object's ID exists in DB.
+ *
+ * @return true if object was created, false if object with ID already exists or other INSERT error was occurred
+ * @see #insert(Object)
+ */
+ boolean create(Pojo object);
+
+ /**
+ * Creates new object, doesn't update.
+ * Calls SELECT to check if object's ID exists in DB.
+ * Use faster {@link #create(Object)} method if saved object is not required
+ *
+ * @return saved object or empty Optional if object with ID already exists or other INSERT error was occurred
+ * @see #create(Object)
+ */
+ Optional createAndGet(Pojo object);
+
+ /**
+ * Create new or update existing object in DB.
+ * Use instead of the slower method {@link #createOrUpdateAndGet(Object)}
+ * if saved object is not required
+ */
+ void createOrUpdate(Pojo object);
+
+ /**
+ * Create new or update existing object in DB.
+ *
+ * @return the saved object (some fields, such as ID, may be set by the DBMS)
+ * @see #createOrUpdate(Object)
+ */
+ Pojo createOrUpdateAndGet(Pojo object);
+
+ void deleteById(ID id);
+}
diff --git a/src/main/java/ru/investbook/api/EventCashFlowRestController.java b/src/main/java/ru/investbook/api/EventCashFlowRestController.java
index b2fb9ce6..fb6fb35c 100644
--- a/src/main/java/ru/investbook/api/EventCashFlowRestController.java
+++ b/src/main/java/ru/investbook/api/EventCashFlowRestController.java
@@ -39,8 +39,6 @@
import ru.investbook.converter.EntityConverter;
import ru.investbook.entity.EventCashFlowEntity;
-import java.util.Optional;
-
@RestController
@Tag(name = "Движения ДС по счету", description = """
Ввод, вывод ДС, налоги, комиссии, а также дивиденды, купоны, амортизации по бумагам другого счета
@@ -99,11 +97,6 @@ public void delete(@PathVariable("id")
super.delete(id);
}
- @Override
- protected Optional getById(Integer id) {
- return repository.findById(id);
- }
-
@Override
protected Integer getId(EventCashFlow object) {
return object.getId();
diff --git a/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java b/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java
index f104bb2f..c67db81e 100644
--- a/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java
+++ b/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java
@@ -22,6 +22,7 @@
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
+import lombok.SneakyThrows;
import org.spacious_team.broker.pojo.ForeignExchangeRate;
import org.springdoc.core.converters.models.PageableAsQueryParam;
import org.springframework.data.domain.Page;
@@ -43,10 +44,8 @@
import ru.investbook.repository.ForeignExchangeRateRepository;
import java.net.URI;
-import java.net.URISyntaxException;
import java.time.LocalDate;
import java.util.List;
-import java.util.Optional;
import java.util.stream.Collectors;
@RestController
@@ -54,6 +53,7 @@
@RequestMapping("/api/v1/foreign-exchange-rates")
public class ForeignExchangeRateRestController extends AbstractRestController {
private final ForeignExchangeRateRepository foreignExchangeRateRepository;
+ private final ForeignExchangeRateConverter foreignExchangeRateConverter;
private final ForeignExchangeRateService foreignExchangeRateService;
public ForeignExchangeRateRestController(ForeignExchangeRateRepository repository,
@@ -61,6 +61,7 @@ public ForeignExchangeRateRestController(ForeignExchangeRateRepository repositor
ForeignExchangeRateService foreignExchangeRateService) {
super(repository, converter);
this.foreignExchangeRateRepository = repository;
+ this.foreignExchangeRateConverter = converter;
this.foreignExchangeRateService = foreignExchangeRateService;
}
@@ -68,8 +69,8 @@ public ForeignExchangeRateRestController(ForeignExchangeRateRepository repositor
@GetMapping
@PageableAsQueryParam
@Operation(summary = "Отобразить все", description = "Отображает все загруженные в БД информацию по обменным курсам")
- protected Page get(@Parameter(hidden = true)
- Pageable pageable) {
+ public Page get(@Parameter(hidden = true)
+ Pageable pageable) {
return super.get(pageable);
}
@@ -81,7 +82,7 @@ protected List get(@PathVariable("currency-pair")
String currencyPair) {
return foreignExchangeRateRepository.findByPkCurrencyPairOrderByPkDateDesc(currencyPair)
.stream()
- .map(converter::fromEntity)
+ .map(foreignExchangeRateConverter::fromEntity)
.collect(Collectors.toList());
}
@@ -143,11 +144,6 @@ public void delete(@PathVariable("currency-pair")
super.delete(getId(currencyPair, date));
}
- @Override
- protected Optional getById(ForeignExchangeRateEntityPk id) {
- return repository.findById(id);
- }
-
@Override
protected ForeignExchangeRateEntityPk getId(ForeignExchangeRate object) {
return getId(object.getCurrencyPair(), object.getDate());
@@ -169,7 +165,8 @@ protected ForeignExchangeRate updateId(ForeignExchangeRateEntityPk id, ForeignEx
}
@Override
- protected URI getLocationURI(ForeignExchangeRate object) throws URISyntaxException {
+ @SneakyThrows
+ protected URI getLocationURI(ForeignExchangeRate object) {
return new URI(getLocation() + "/currency-pairs/" + object.getCurrencyPair() + "/dates/" + object.getDate());
}
diff --git a/src/main/java/ru/investbook/api/IssuerRestController.java b/src/main/java/ru/investbook/api/IssuerRestController.java
index f5ac24cb..f944dd5b 100644
--- a/src/main/java/ru/investbook/api/IssuerRestController.java
+++ b/src/main/java/ru/investbook/api/IssuerRestController.java
@@ -39,8 +39,6 @@
import ru.investbook.converter.EntityConverter;
import ru.investbook.entity.IssuerEntity;
-import java.util.Optional;
-
@RestController
@Tag(name = "Эмитенты", description = "Информация об эмитентах")
@RequestMapping("/api/v1/issuers")
@@ -96,11 +94,6 @@ public void delete(@PathVariable("id")
super.delete(id);
}
- @Override
- protected Optional getById(Integer id) {
- return repository.findById(id);
- }
-
@Override
protected Integer getId(Issuer object) {
return object.getId();
diff --git a/src/main/java/ru/investbook/api/PortfolioCashRestController.java b/src/main/java/ru/investbook/api/PortfolioCashRestController.java
index 676eff3f..6d07caa1 100644
--- a/src/main/java/ru/investbook/api/PortfolioCashRestController.java
+++ b/src/main/java/ru/investbook/api/PortfolioCashRestController.java
@@ -39,8 +39,6 @@
import ru.investbook.converter.EntityConverter;
import ru.investbook.entity.PortfolioCashEntity;
-import java.util.Optional;
-
@RestController
@Tag(name = "Информация по остатку денежных средств на счете")
@RequestMapping("/api/v1/portfolio-cash")
@@ -97,11 +95,6 @@ public void delete(@PathVariable("id")
super.delete(id);
}
- @Override
- protected Optional getById(Integer id) {
- return repository.findById(id);
- }
-
@Override
protected Integer getId(PortfolioCash object) {
return object.getId();
diff --git a/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java b/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java
index 3c038197..3bd17ec4 100644
--- a/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java
+++ b/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java
@@ -39,8 +39,6 @@
import ru.investbook.converter.EntityConverter;
import ru.investbook.entity.PortfolioPropertyEntity;
-import java.util.Optional;
-
@RestController
@Tag(name = "Информация по счетам")
@RequestMapping("/api/v1/portfolio-properties")
@@ -97,11 +95,6 @@ public void delete(@PathVariable("id")
super.delete(id);
}
- @Override
- protected Optional getById(Integer id) {
- return repository.findById(id);
- }
-
@Override
protected Integer getId(PortfolioProperty object) {
return object.getId();
diff --git a/src/main/java/ru/investbook/api/PortfolioRestController.java b/src/main/java/ru/investbook/api/PortfolioRestController.java
index 83af4c0d..04f4e700 100644
--- a/src/main/java/ru/investbook/api/PortfolioRestController.java
+++ b/src/main/java/ru/investbook/api/PortfolioRestController.java
@@ -39,8 +39,6 @@
import ru.investbook.entity.PortfolioEntity;
import ru.investbook.repository.PortfolioRepository;
-import java.util.Optional;
-
@RestController
@Tag(name = "Счета")
@RequestMapping("/api/v1/portfolios")
@@ -98,11 +96,6 @@ public void delete(@PathVariable("id")
super.delete(id);
}
- @Override
- protected Optional getById(String id) {
- return repository.findById(id);
- }
-
@Override
protected String getId(Portfolio object) {
return object.getId();
diff --git a/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java b/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java
index 4f5c394c..40ebe653 100644
--- a/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java
@@ -39,8 +39,6 @@
import ru.investbook.entity.SecurityDescriptionEntity;
import ru.investbook.repository.SecurityDescriptionRepository;
-import java.util.Optional;
-
@RestController
@Tag(name = "Информация по инструментам", description = "Сектор экономики, эмитент")
@RequestMapping("/api/v1/security-descriptions")
@@ -102,11 +100,6 @@ public void delete(@PathVariable("id")
super.delete(id);
}
- @Override
- protected Optional getById(Integer isin) {
- return repository.findById(isin);
- }
-
@Override
protected Integer getId(SecurityDescription object) {
return object.getSecurity();
diff --git a/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java b/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java
index d4348a6f..63a43909 100644
--- a/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java
@@ -40,8 +40,6 @@
import ru.investbook.entity.SecurityEventCashFlowEntity;
import ru.investbook.report.FifoPositionsFactory;
-import java.util.Optional;
-
import static org.spacious_team.broker.pojo.CashFlowType.REDEMPTION;
@RestController
@@ -108,11 +106,6 @@ public void delete(@PathVariable("id")
super.delete(id);
}
- @Override
- protected Optional getById(Integer id) {
- return repository.findById(id);
- }
-
@Override
protected Integer getId(SecurityEventCashFlow object) {
return object.getId();
diff --git a/src/main/java/ru/investbook/api/SecurityQuoteRestController.java b/src/main/java/ru/investbook/api/SecurityQuoteRestController.java
index 24b6f784..538279eb 100644
--- a/src/main/java/ru/investbook/api/SecurityQuoteRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityQuoteRestController.java
@@ -39,8 +39,6 @@
import ru.investbook.converter.EntityConverter;
import ru.investbook.entity.SecurityQuoteEntity;
-import java.util.Optional;
-
@RestController
@Tag(name = "Котировки", description = "Котировки биржевых инструментов")
@RequestMapping("/api/v1/security-quotes")
@@ -98,11 +96,6 @@ public void delete(@PathVariable("id")
super.delete(id);
}
- @Override
- protected Optional getById(Integer id) {
- return repository.findById(id);
- }
-
@Override
protected Integer getId(SecurityQuote object) {
return object.getId();
diff --git a/src/main/java/ru/investbook/api/SecurityRestController.java b/src/main/java/ru/investbook/api/SecurityRestController.java
index 78480b5c..7d7efb8a 100644
--- a/src/main/java/ru/investbook/api/SecurityRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityRestController.java
@@ -39,8 +39,6 @@
import ru.investbook.entity.SecurityEntity;
import ru.investbook.repository.SecurityRepository;
-import java.util.Optional;
-
@RestController
@Tag(name = "Инструменты", description = "Акции, облигации, деривативы и валютные пары")
@RequestMapping("/api/v1/securities")
@@ -101,11 +99,6 @@ public void delete(@PathVariable("id")
super.delete(id);
}
- @Override
- protected Optional getById(Integer id) {
- return repository.findById(id);
- }
-
@Override
protected Integer getId(Security object) {
return object.getId();
diff --git a/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java b/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java
index 58b025cd..983d9a2e 100644
--- a/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java
+++ b/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java
@@ -44,7 +44,6 @@
import ru.investbook.repository.TransactionCashFlowRepository;
import java.util.List;
-import java.util.Optional;
@RestController
@Tag(name = "Движения ДС по сделкам", description = "Уплаченные и вырученные суммы в сделках")
@@ -144,11 +143,6 @@ public void delete(@PathVariable("id")
super.delete(id);
}
- @Override
- protected Optional getById(Integer id) {
- return repository.findById(id);
- }
-
@Override
protected Integer getId(TransactionCashFlow object) {
return object.getId();
diff --git a/src/main/java/ru/investbook/api/TransactionRestController.java b/src/main/java/ru/investbook/api/TransactionRestController.java
index b1c44124..1e40e4ea 100644
--- a/src/main/java/ru/investbook/api/TransactionRestController.java
+++ b/src/main/java/ru/investbook/api/TransactionRestController.java
@@ -43,7 +43,6 @@
import ru.investbook.repository.TransactionRepository;
import java.util.List;
-import java.util.Optional;
@RestController
@Tag(name = "Сделки", description = "Операции купли/продажи биржевых инструментов")
@@ -151,11 +150,6 @@ public void delete(@PathVariable("id")
super.delete(id);
}
- @Override
- protected Optional getById(Integer id) {
- return repository.findById(id);
- }
-
@Override
protected Integer getId(Transaction object) {
return object.getId();
diff --git a/src/main/java/ru/investbook/parser/InvestbookApiClient.java b/src/main/java/ru/investbook/parser/InvestbookApiClient.java
index 4a4952ed..e0458bc9 100644
--- a/src/main/java/ru/investbook/parser/InvestbookApiClient.java
+++ b/src/main/java/ru/investbook/parser/InvestbookApiClient.java
@@ -120,6 +120,7 @@ public void addTransaction(AbstractTransaction transaction) {
}
private Optional getSavedTransactionId(AbstractTransaction transaction) {
+ // todo replace RestController with EntityRepositoryService
return Optional.of(transactionRestController.get(transaction.getPortfolio(), transaction.getTradeId(), Pageable.unpaged()).getContent())
.filter(result -> result.size() == 1)
.map(List::getFirst)
@@ -196,6 +197,7 @@ public void addForeignExchangeRate(ForeignExchangeRate exchangeRate) {
/**
* @return true if new row was added, or it was already exists in DB, false - or error
*/
+ // todo replace RestController with EntityRepositoryService
private boolean handlePost(T object, Function> saver, String errorPrefix) {
try {
validator.validate(object);
From 95918b0282af5aa3b091ad4d15a6fd2d1941436f Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sat, 20 Jul 2024 15:57:53 +0300
Subject: [PATCH 23/40] add AbstractEntityRepositoryServiceTest
---
.../AbstractEntityRepositoryServiceTest.java | 76 +++++++++++++++++++
1 file changed, 76 insertions(+)
create mode 100644 src/test/java/ru/investbook/api/AbstractEntityRepositoryServiceTest.java
diff --git a/src/test/java/ru/investbook/api/AbstractEntityRepositoryServiceTest.java b/src/test/java/ru/investbook/api/AbstractEntityRepositoryServiceTest.java
new file mode 100644
index 00000000..d410acfb
--- /dev/null
+++ b/src/test/java/ru/investbook/api/AbstractEntityRepositoryServiceTest.java
@@ -0,0 +1,76 @@
+/*
+ * InvestBook
+ * Copyright (C) 2024 Spacious Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package ru.investbook.api;
+
+import org.junit.jupiter.api.Test;
+import org.spacious_team.broker.pojo.Security;
+import org.spacious_team.broker.pojo.SecurityType;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.time.Duration;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+/**
+ * Performance test
+ */
+@SpringBootTest(properties = "spring.datasource.url=jdbc:h2:mem:testdb;mode=mysql;non_keywords=value")
+class AbstractEntityRepositoryServiceTest {
+
+ @Autowired
+ private SecurityRestController service;
+ private final Random random = new Random();
+ private final AtomicInteger i = new AtomicInteger();
+
+ @Test
+ void insert() {
+ test("insert()", service::insert, false);
+ test("insert()", service::insert, false);
+ test("insert()", service::insert, true);
+ test("insert()", service::insert, true);
+ }
+
+ @Test
+ void create() {
+ test("create()", service::create, false);
+ test("create()", service::create, false);
+ test("create()", service::create, true);
+ test("create()", service::create, true);
+ }
+
+ void test(String name, Consumer consumer, boolean isIdNull) {
+ long t0 = System.nanoTime();
+ for (int i = 0; i < 1_000; i++) {
+ Security security = createSecurity(isIdNull);
+ consumer.accept(security);
+ }
+ String isIdNullMsg = isIdNull ? "without predefined ID" : "with always same ID (insert error)";
+ System.out.println("Total time " + name + " (" + isIdNullMsg + "):" + Duration.ofNanos(System.nanoTime() - t0));
+ }
+
+ Security createSecurity(boolean isIdNull) {
+ return Security.builder()
+ .id(isIdNull ? null : 1)
+ .type(SecurityType.STOCK)
+ .name(String.valueOf(i.getAndIncrement()))
+ .build();
+ }
+}
\ No newline at end of file
From 5412be0bcf7fbe83d61a0197e108f42091e86fad Mon Sep 17 00:00:00 2001
From: vananiev
Date: Mon, 22 Jul 2024 00:01:40 +0300
Subject: [PATCH 24/40] restore correct Created status for http PUT method,
change OK to No Content for PUT update case
---
.../api/AbstractRestController.java | 29 ++++++++++---------
1 file changed, 15 insertions(+), 14 deletions(-)
diff --git a/src/main/java/ru/investbook/api/AbstractRestController.java b/src/main/java/ru/investbook/api/AbstractRestController.java
index a70b58b8..5c4634e0 100644
--- a/src/main/java/ru/investbook/api/AbstractRestController.java
+++ b/src/main/java/ru/investbook/api/AbstractRestController.java
@@ -31,6 +31,7 @@
import java.util.Objects;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.nonNull;
public abstract class AbstractRestController extends AbstractEntityRepositoryService {
@@ -50,8 +51,8 @@ public ResponseEntity get(ID id) {
/**
* Creates a new entity.
- * If entity has ID and record with this ID already exists in DB, CONFLICT http status and Location header was returned.
- * Otherwise, CREATE http status will be returned with Location header.
+ * If entity has ID and record with this ID already exists in DB, "409 Conflict" http status and Location header was returned.
+ * Otherwise, "201 Created" http status will be returned with Location header.
* When creating new object, ID may be passed, but that ID only used when no {@link GeneratedValue} set
* on Entity ID field or if {@link GeneratedValue#generator} set to {@link org.hibernate.generator.BeforeExecutionGenerator},
* generator impl; otherwise ID, passed in object, will be ignored (see JPA impl).
@@ -75,8 +76,8 @@ protected ResponseEntity post(Pojo object) {
/**
* Updates or creates a new entity.
- * In update case method returns OK http status.
- * In create case method returns CREATE http status and Location header.
+ * In create case method returns "201 Created" http status and Location header.
+ * In update case method returns "204 No Content" http status.
* When creating new object, ID may be passed in the object, but it should be same as {@code id} argument.
*
* @param id updating or creating entity id
@@ -87,18 +88,18 @@ protected ResponseEntity post(Pojo object) {
@Transactional
public ResponseEntity put(ID id, Pojo object) {
try {
- if (getId(object) == null) {
- object = updateId(id, object);
- } else if (!Objects.equals(id, getId(object))) {
+ ID objectId = getId(object);
+ if (nonNull(objectId) && !Objects.equals(id, objectId)) {
throw new BadRequestException("Идентификатор объекта, переданный в URI [" + id + "] и в теле " +
- "запроса [" + getId(object) + "] не совпадают");
- }
- if (create(object)) {
- return ResponseEntity.ok().build(); // todo no content
- } else {
- Pojo savedObject = createOrUpdateAndGet(object);
- return createResponseWithLocationHeader(savedObject); // todo change http status?
+ "запроса [" + objectId + "] не совпадают");
}
+ Pojo objectWithId = nonNull(objectId) ? object : updateId(id, object);
+ return createAndGet(objectWithId)
+ .map(this::createResponseWithLocationHeader)
+ .orElseGet(() -> {
+ createOrUpdate(objectWithId);
+ return ResponseEntity.noContent().build();
+ });
} catch (Exception e) {
throw new InternalServerErrorException("Не могу создать объект", e);
}
From 43aa72724c6ada487b411c660d3619d86a467531 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Mon, 22 Jul 2024 00:14:19 +0300
Subject: [PATCH 25/40] return 204 No Content by http delete method
---
src/main/java/ru/investbook/api/AbstractRestController.java | 6 +++---
.../java/ru/investbook/api/EventCashFlowRestController.java | 4 ++--
.../investbook/api/ForeignExchangeRateRestController.java | 4 ++--
src/main/java/ru/investbook/api/IssuerRestController.java | 4 ++--
.../java/ru/investbook/api/PortfolioCashRestController.java | 4 ++--
.../ru/investbook/api/PortfolioPropertyRestController.java | 4 ++--
.../java/ru/investbook/api/PortfolioRestController.java | 4 ++--
.../investbook/api/SecurityDescriptionRestController.java | 4 ++--
.../investbook/api/SecurityEventCashFlowRestController.java | 4 ++--
.../java/ru/investbook/api/SecurityQuoteRestController.java | 4 ++--
src/main/java/ru/investbook/api/SecurityRestController.java | 4 ++--
.../investbook/api/TransactionCashFlowRestController.java | 4 ++--
.../java/ru/investbook/api/TransactionRestController.java | 4 ++--
13 files changed, 27 insertions(+), 27 deletions(-)
diff --git a/src/main/java/ru/investbook/api/AbstractRestController.java b/src/main/java/ru/investbook/api/AbstractRestController.java
index 5c4634e0..1c5b66cd 100644
--- a/src/main/java/ru/investbook/api/AbstractRestController.java
+++ b/src/main/java/ru/investbook/api/AbstractRestController.java
@@ -106,11 +106,11 @@ public ResponseEntity put(ID id, Pojo object) {
}
/**
- * Deletes object from storage. Always return OK http status with empty body.
+ * Deletes object from storage. Always return "204 No Content" http status with empty body.
*/
- // TODO should impl 404 status? https://stackoverflow.com/questions/4088350/is-rest-delete-really-idempotent
- public void delete(ID id) {
+ public ResponseEntity delete(ID id) {
deleteById(id);
+ return ResponseEntity.noContent().build();
}
/**
diff --git a/src/main/java/ru/investbook/api/EventCashFlowRestController.java b/src/main/java/ru/investbook/api/EventCashFlowRestController.java
index fb6fb35c..a1c76265 100644
--- a/src/main/java/ru/investbook/api/EventCashFlowRestController.java
+++ b/src/main/java/ru/investbook/api/EventCashFlowRestController.java
@@ -91,10 +91,10 @@ public ResponseEntity put(@PathVariable("id")
@Override
@DeleteMapping("{id}")
@Operation(summary = "Удалить", description = "Удалить информацию из БД")
- public void delete(@PathVariable("id")
+ public ResponseEntity delete(@PathVariable("id")
@Parameter(description = "Номер события")
Integer id) {
- super.delete(id);
+ return super.delete(id);
}
@Override
diff --git a/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java b/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java
index c67db81e..80f91f39 100644
--- a/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java
+++ b/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java
@@ -133,7 +133,7 @@ public ResponseEntity put(@PathVariable("currency-pair")
*/
@DeleteMapping("/currency-pairs/{currency-pair}/dates/{date}")
@Operation(summary = "Удалить", description = "Удаляет информацию о курсе из БД")
- public void delete(@PathVariable("currency-pair")
+ public ResponseEntity delete(@PathVariable("currency-pair")
@Parameter(description = "Валютная пара", example = "USDRUB")
String currencyPair,
@PathVariable("date")
@@ -141,7 +141,7 @@ public void delete(@PathVariable("currency-pair")
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate date) {
foreignExchangeRateService.invalidateCache();
- super.delete(getId(currencyPair, date));
+ return super.delete(getId(currencyPair, date));
}
@Override
diff --git a/src/main/java/ru/investbook/api/IssuerRestController.java b/src/main/java/ru/investbook/api/IssuerRestController.java
index f944dd5b..e2211ef8 100644
--- a/src/main/java/ru/investbook/api/IssuerRestController.java
+++ b/src/main/java/ru/investbook/api/IssuerRestController.java
@@ -88,10 +88,10 @@ public ResponseEntity put(@PathVariable("id")
@Override
@DeleteMapping("{id}")
@Operation(summary = "Удалить", description = "Удаляет сведения об эмитенте из БД")
- public void delete(@PathVariable("id")
+ public ResponseEntity delete(@PathVariable("id")
@Parameter(description = "Внутренний идентификатор эмитента")
Integer id) {
- super.delete(id);
+ return super.delete(id);
}
@Override
diff --git a/src/main/java/ru/investbook/api/PortfolioCashRestController.java b/src/main/java/ru/investbook/api/PortfolioCashRestController.java
index 6d07caa1..5d39a352 100644
--- a/src/main/java/ru/investbook/api/PortfolioCashRestController.java
+++ b/src/main/java/ru/investbook/api/PortfolioCashRestController.java
@@ -89,10 +89,10 @@ public ResponseEntity put(@PathVariable("id")
@Override
@DeleteMapping("{id}")
@Operation(summary = "Удалить")
- public void delete(@PathVariable("id")
+ public ResponseEntity delete(@PathVariable("id")
@Parameter(description = "Внутренний идентификатор записи")
Integer id) {
- super.delete(id);
+ return super.delete(id);
}
@Override
diff --git a/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java b/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java
index 3bd17ec4..0f2c016c 100644
--- a/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java
+++ b/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java
@@ -89,10 +89,10 @@ public ResponseEntity put(@PathVariable("id")
@Override
@DeleteMapping("{id}")
@Operation(summary = "Удалить")
- public void delete(@PathVariable("id")
+ public ResponseEntity delete(@PathVariable("id")
@Parameter(description = "Внутренний идентификатор записи")
Integer id) {
- super.delete(id);
+ return super.delete(id);
}
@Override
diff --git a/src/main/java/ru/investbook/api/PortfolioRestController.java b/src/main/java/ru/investbook/api/PortfolioRestController.java
index 04f4e700..6c5caa8a 100644
--- a/src/main/java/ru/investbook/api/PortfolioRestController.java
+++ b/src/main/java/ru/investbook/api/PortfolioRestController.java
@@ -90,10 +90,10 @@ public ResponseEntity put(@PathVariable("id")
@Override
@DeleteMapping("{id}")
@Operation(summary = "Удалить", description = "Удалить счет и все связанные с ним данные")
- public void delete(@PathVariable("id")
+ public ResponseEntity delete(@PathVariable("id")
@Parameter(description = "Номер счета")
String id) {
- super.delete(id);
+ return super.delete(id);
}
@Override
diff --git a/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java b/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java
index 40ebe653..0f74f195 100644
--- a/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java
@@ -94,10 +94,10 @@ public ResponseEntity put(@PathVariable("id")
@Override
@DeleteMapping("{id}")
@Operation(summary = "Удалить", description = "Удалить информацию по инструменту")
- public void delete(@PathVariable("id")
+ public ResponseEntity delete(@PathVariable("id")
@Parameter(description = "Идентификатор", example = "123", required = true)
Integer id) {
- super.delete(id);
+ return super.delete(id);
}
@Override
diff --git a/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java b/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java
index 63a43909..98ebe98f 100644
--- a/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java
@@ -99,11 +99,11 @@ public ResponseEntity put(@PathVariable("id")
@Override
@DeleteMapping("{id}")
@Operation(summary = "Удалить", description = "Удалить информацию о выплате из БД")
- public void delete(@PathVariable("id")
+ public ResponseEntity delete(@PathVariable("id")
@Parameter(description = "Внутренний идентификатор выплаты в БД")
Integer id) {
positionsFactory.invalidateCache();
- super.delete(id);
+ return super.delete(id);
}
@Override
diff --git a/src/main/java/ru/investbook/api/SecurityQuoteRestController.java b/src/main/java/ru/investbook/api/SecurityQuoteRestController.java
index 538279eb..227ed206 100644
--- a/src/main/java/ru/investbook/api/SecurityQuoteRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityQuoteRestController.java
@@ -90,10 +90,10 @@ public ResponseEntity put(@PathVariable("id")
@Override
@DeleteMapping("{id}")
@Operation(summary = "Удалить")
- public void delete(@PathVariable("id")
+ public ResponseEntity delete(@PathVariable("id")
@Parameter(description = "Номер записи о котировке")
Integer id) {
- super.delete(id);
+ return super.delete(id);
}
@Override
diff --git a/src/main/java/ru/investbook/api/SecurityRestController.java b/src/main/java/ru/investbook/api/SecurityRestController.java
index 7d7efb8a..05e9594c 100644
--- a/src/main/java/ru/investbook/api/SecurityRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityRestController.java
@@ -93,10 +93,10 @@ public ResponseEntity put(@PathVariable("id")
@Override
@DeleteMapping("{id}")
@Operation(summary = "Удалить", description = "Удалить сведения о биржевом инструменте и всех его сделках по всем счетам")
- public void delete(@PathVariable("id")
+ public ResponseEntity delete(@PathVariable("id")
@Parameter(description = "Идентификатор", example = "123", required = true)
Integer id) {
- super.delete(id);
+ return super.delete(id);
}
@Override
diff --git a/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java b/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java
index 983d9a2e..8be6772e 100644
--- a/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java
+++ b/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java
@@ -137,10 +137,10 @@ public ResponseEntity put(@PathVariable("id")
@Operation(summary = "Удалить", description = """
Удалить информацию об об объемах движения ДС по сделке. Сама сделка не удаляется, ее нужно удалить своим API
""")
- public void delete(@PathVariable("id")
+ public ResponseEntity delete(@PathVariable("id")
@Parameter(description = "Внутренний идентификатор сделки")
Integer id) {
- super.delete(id);
+ return super.delete(id);
}
@Override
diff --git a/src/main/java/ru/investbook/api/TransactionRestController.java b/src/main/java/ru/investbook/api/TransactionRestController.java
index 1e40e4ea..fad619cd 100644
--- a/src/main/java/ru/investbook/api/TransactionRestController.java
+++ b/src/main/java/ru/investbook/api/TransactionRestController.java
@@ -143,11 +143,11 @@ public ResponseEntity put(@PathVariable("id")
@Override
@DeleteMapping("{id}")
@Operation(summary = "Удалить", description = "Удаляет указанную сделку")
- public void delete(@PathVariable("id")
+ public ResponseEntity delete(@PathVariable("id")
@Parameter(description = "Внутренний идентификатор сделки")
Integer id) {
positionsFactory.invalidateCache();
- super.delete(id);
+ return super.delete(id);
}
@Override
From 8300dfd29c5f26cac95fcb3f94f453c6768a56d4 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Fri, 26 Jul 2024 18:54:26 +0300
Subject: [PATCH 26/40] change EntityRepositoryService interface
---
.../api/AbstractEntityRepositoryService.java | 4 +---
.../investbook/api/AbstractRestController.java | 6 ++++++
.../investbook/api/EntityRepositoryService.java | 16 ++++++++++++----
.../api/EventCashFlowRestController.java | 2 +-
.../api/ForeignExchangeRateRestController.java | 2 +-
.../ru/investbook/api/IssuerRestController.java | 2 +-
.../api/PortfolioCashRestController.java | 2 +-
.../api/PortfolioPropertyRestController.java | 2 +-
.../investbook/api/PortfolioRestController.java | 2 +-
.../api/SecurityDescriptionRestController.java | 2 +-
.../api/SecurityEventCashFlowRestController.java | 2 +-
.../api/SecurityQuoteRestController.java | 2 +-
.../investbook/api/SecurityRestController.java | 2 +-
.../api/TransactionCashFlowRestController.java | 2 +-
.../api/TransactionRestController.java | 2 +-
15 files changed, 31 insertions(+), 19 deletions(-)
diff --git a/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java b/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java
index 6541e33a..61652ae6 100644
--- a/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java
+++ b/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java
@@ -68,7 +68,7 @@ public Optional getById(ID id) {
}
@Override
- public Page get(Pageable pageable) {
+ public Page getPage(Pageable pageable) {
return repository.findAll(pageable)
.map(converter::fromEntity);
}
@@ -146,6 +146,4 @@ private Entity createOrUpdateInternal(Pojo object) {
public void deleteById(ID id) {
repository.deleteById(id);
}
-
- protected abstract ID getId(Pojo object);
}
diff --git a/src/main/java/ru/investbook/api/AbstractRestController.java b/src/main/java/ru/investbook/api/AbstractRestController.java
index 1c5b66cd..f5092568 100644
--- a/src/main/java/ru/investbook/api/AbstractRestController.java
+++ b/src/main/java/ru/investbook/api/AbstractRestController.java
@@ -20,6 +20,8 @@
import jakarta.persistence.GeneratedValue;
import lombok.SneakyThrows;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -39,6 +41,10 @@ protected AbstractRestController(JpaRepository repository, EntityCon
super(repository, converter);
}
+ public Page get(Pageable pageable) {
+ return getPage(pageable);
+ }
+
/**
* Gets the entity.
* If entity not exists NOT_FOUND http status will be returned.
diff --git a/src/main/java/ru/investbook/api/EntityRepositoryService.java b/src/main/java/ru/investbook/api/EntityRepositoryService.java
index 0a9e46c8..6c51c47c 100644
--- a/src/main/java/ru/investbook/api/EntityRepositoryService.java
+++ b/src/main/java/ru/investbook/api/EntityRepositoryService.java
@@ -25,17 +25,20 @@
public interface EntityRepositoryService {
+ ID getId(Pojo object);
+
boolean existsById(ID id);
Optional getById(ID id);
- Page get(Pageable pageable);
+ Page getPage(Pageable pageable);
/**
* Creates a new object with direct INSERT into DB (without prior SELECT call) if possible,
* calls {@link #create(Object)} otherwise.
*
- * @return true if object is created or false otherwise
+ * @return true if object is created, false if object with ID already exists
+ * @throws RuntimeException if an INSERT error occurs
* @implNote Method performance is the same as {@link #create(Object)} for H2 2.2.224 and MariaDB 11.2
*/
boolean insert(Pojo object);
@@ -44,7 +47,8 @@ public interface EntityRepositoryService {
* Creates new object, doesn't update.
* Calls SELECT to check if object's ID exists in DB.
*
- * @return true if object was created, false if object with ID already exists or other INSERT error was occurred
+ * @return true if object was created, false if object with ID already exists
+ * @throws RuntimeException if an INSERT error occurs
* @see #insert(Object)
*/
boolean create(Pojo object);
@@ -54,7 +58,8 @@ public interface EntityRepositoryService {
* Calls SELECT to check if object's ID exists in DB.
* Use faster {@link #create(Object)} method if saved object is not required
*
- * @return saved object or empty Optional if object with ID already exists or other INSERT error was occurred
+ * @return created object or empty Optional if object with ID already exists
+ * @throws RuntimeException if an INSERT error occurs
* @see #create(Object)
*/
Optional createAndGet(Pojo object);
@@ -63,6 +68,8 @@ public interface EntityRepositoryService {
* Create new or update existing object in DB.
* Use instead of the slower method {@link #createOrUpdateAndGet(Object)}
* if saved object is not required
+ *
+ * @throws RuntimeException if underlying DB error occurs
*/
void createOrUpdate(Pojo object);
@@ -70,6 +77,7 @@ public interface EntityRepositoryService {
* Create new or update existing object in DB.
*
* @return the saved object (some fields, such as ID, may be set by the DBMS)
+ * @throws RuntimeException if underlying DB error occurs
* @see #createOrUpdate(Object)
*/
Pojo createOrUpdateAndGet(Pojo object);
diff --git a/src/main/java/ru/investbook/api/EventCashFlowRestController.java b/src/main/java/ru/investbook/api/EventCashFlowRestController.java
index a1c76265..236dbbae 100644
--- a/src/main/java/ru/investbook/api/EventCashFlowRestController.java
+++ b/src/main/java/ru/investbook/api/EventCashFlowRestController.java
@@ -98,7 +98,7 @@ public ResponseEntity delete(@PathVariable("id")
}
@Override
- protected Integer getId(EventCashFlow object) {
+ public Integer getId(EventCashFlow object) {
return object.getId();
}
diff --git a/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java b/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java
index 80f91f39..8e962a79 100644
--- a/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java
+++ b/src/main/java/ru/investbook/api/ForeignExchangeRateRestController.java
@@ -145,7 +145,7 @@ public ResponseEntity delete(@PathVariable("currency-pair")
}
@Override
- protected ForeignExchangeRateEntityPk getId(ForeignExchangeRate object) {
+ public ForeignExchangeRateEntityPk getId(ForeignExchangeRate object) {
return getId(object.getCurrencyPair(), object.getDate());
}
diff --git a/src/main/java/ru/investbook/api/IssuerRestController.java b/src/main/java/ru/investbook/api/IssuerRestController.java
index e2211ef8..6a6ccedb 100644
--- a/src/main/java/ru/investbook/api/IssuerRestController.java
+++ b/src/main/java/ru/investbook/api/IssuerRestController.java
@@ -95,7 +95,7 @@ public ResponseEntity delete(@PathVariable("id")
}
@Override
- protected Integer getId(Issuer object) {
+ public Integer getId(Issuer object) {
return object.getId();
}
diff --git a/src/main/java/ru/investbook/api/PortfolioCashRestController.java b/src/main/java/ru/investbook/api/PortfolioCashRestController.java
index 5d39a352..b74650ef 100644
--- a/src/main/java/ru/investbook/api/PortfolioCashRestController.java
+++ b/src/main/java/ru/investbook/api/PortfolioCashRestController.java
@@ -96,7 +96,7 @@ public ResponseEntity delete(@PathVariable("id")
}
@Override
- protected Integer getId(PortfolioCash object) {
+ public Integer getId(PortfolioCash object) {
return object.getId();
}
diff --git a/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java b/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java
index 0f2c016c..efe5fa4c 100644
--- a/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java
+++ b/src/main/java/ru/investbook/api/PortfolioPropertyRestController.java
@@ -96,7 +96,7 @@ public ResponseEntity delete(@PathVariable("id")
}
@Override
- protected Integer getId(PortfolioProperty object) {
+ public Integer getId(PortfolioProperty object) {
return object.getId();
}
diff --git a/src/main/java/ru/investbook/api/PortfolioRestController.java b/src/main/java/ru/investbook/api/PortfolioRestController.java
index 6c5caa8a..f9637127 100644
--- a/src/main/java/ru/investbook/api/PortfolioRestController.java
+++ b/src/main/java/ru/investbook/api/PortfolioRestController.java
@@ -97,7 +97,7 @@ public ResponseEntity delete(@PathVariable("id")
}
@Override
- protected String getId(Portfolio object) {
+ public String getId(Portfolio object) {
return object.getId();
}
diff --git a/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java b/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java
index 0f74f195..1112b9ee 100644
--- a/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityDescriptionRestController.java
@@ -101,7 +101,7 @@ public ResponseEntity delete(@PathVariable("id")
}
@Override
- protected Integer getId(SecurityDescription object) {
+ public Integer getId(SecurityDescription object) {
return object.getSecurity();
}
diff --git a/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java b/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java
index 98ebe98f..9d5edbf9 100644
--- a/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityEventCashFlowRestController.java
@@ -107,7 +107,7 @@ public ResponseEntity delete(@PathVariable("id")
}
@Override
- protected Integer getId(SecurityEventCashFlow object) {
+ public Integer getId(SecurityEventCashFlow object) {
return object.getId();
}
diff --git a/src/main/java/ru/investbook/api/SecurityQuoteRestController.java b/src/main/java/ru/investbook/api/SecurityQuoteRestController.java
index 227ed206..bac27a4c 100644
--- a/src/main/java/ru/investbook/api/SecurityQuoteRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityQuoteRestController.java
@@ -97,7 +97,7 @@ public ResponseEntity delete(@PathVariable("id")
}
@Override
- protected Integer getId(SecurityQuote object) {
+ public Integer getId(SecurityQuote object) {
return object.getId();
}
diff --git a/src/main/java/ru/investbook/api/SecurityRestController.java b/src/main/java/ru/investbook/api/SecurityRestController.java
index 05e9594c..28f7e377 100644
--- a/src/main/java/ru/investbook/api/SecurityRestController.java
+++ b/src/main/java/ru/investbook/api/SecurityRestController.java
@@ -100,7 +100,7 @@ public ResponseEntity delete(@PathVariable("id")
}
@Override
- protected Integer getId(Security object) {
+ public Integer getId(Security object) {
return object.getId();
}
diff --git a/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java b/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java
index 8be6772e..b09d4dea 100644
--- a/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java
+++ b/src/main/java/ru/investbook/api/TransactionCashFlowRestController.java
@@ -144,7 +144,7 @@ public ResponseEntity delete(@PathVariable("id")
}
@Override
- protected Integer getId(TransactionCashFlow object) {
+ public Integer getId(TransactionCashFlow object) {
return object.getId();
}
diff --git a/src/main/java/ru/investbook/api/TransactionRestController.java b/src/main/java/ru/investbook/api/TransactionRestController.java
index fad619cd..cac5a36a 100644
--- a/src/main/java/ru/investbook/api/TransactionRestController.java
+++ b/src/main/java/ru/investbook/api/TransactionRestController.java
@@ -151,7 +151,7 @@ public ResponseEntity delete(@PathVariable("id")
}
@Override
- protected Integer getId(Transaction object) {
+ public Integer getId(Transaction object) {
return object.getId();
}
From e71624c8531412c72b598349ef3125296fefa5ec Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sat, 27 Jul 2024 14:53:42 +0300
Subject: [PATCH 27/40] refactor InvestbookApiClient.saveWithoutUpdate()
---
.../api/AbstractEntityRepositoryService.java | 16 +++--
.../api/AbstractRestController.java | 4 +-
.../api/EntityRepositoryService.java | 17 +++--
.../parser/InvestbookApiClient.java | 65 +++++++++----------
.../AbstractEntityRepositoryServiceTest.java | 10 +--
5 files changed, 56 insertions(+), 56 deletions(-)
diff --git a/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java b/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java
index 61652ae6..1d8362fc 100644
--- a/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java
+++ b/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java
@@ -73,12 +73,16 @@ public Page getPage(Pageable pageable) {
.map(converter::fromEntity);
}
+ /**
+ * @implNote Method performance is the same as {@link #createIfAbsent(Object)} for H2 2.2.224 and MariaDB 11.2
+ */
@Override
@SuppressWarnings("deprecation")
public boolean insert(Pojo object) {
if (entityManager instanceof Session hibernateSpecificSession) {
try {
Entity entity = converter.toEntity(object);
+ // Hibernate save() method does sql INSERT
transactionTemplateRequiresNew.executeWithoutResult(_ -> hibernateSpecificSession.save(entity));
return true;
} catch (Exception e) {
@@ -88,21 +92,21 @@ public boolean insert(Pojo object) {
log.error("Can't INSERT by optimized deprecated Hibernate method save(): {}", object, e);
}
}
- Boolean result = transactionTemplateRequired.execute(_ -> create(object));
+ Boolean result = transactionTemplateRequired.execute(_ -> createIfAbsent(object));
return Boolean.TRUE.equals(result);
}
@Override
@Transactional
- public boolean create(Pojo object) {
- return createInternal(object)
+ public boolean createIfAbsent(Pojo object) {
+ return createIfAbsentInternal(object)
.isPresent();
}
@Override
@Transactional
- public Optional createAndGet(Pojo object) {
- return createInternal(object)
+ public Optional createAndGetIfAbsent(Pojo object) {
+ return createIfAbsentInternal(object)
.map(converter::fromEntity);
}
@@ -112,7 +116,7 @@ public Optional createAndGet(Pojo object) {
* @return created entity if object is created or empty Optional otherwise
* @implSpec Should be called in transaction
*/
- private Optional createInternal(Pojo object) {
+ private Optional createIfAbsentInternal(Pojo object) {
ID id = getId(object);
if (id != null && existsById(id)) {
return Optional.empty();
diff --git a/src/main/java/ru/investbook/api/AbstractRestController.java b/src/main/java/ru/investbook/api/AbstractRestController.java
index f5092568..6c25c9ec 100644
--- a/src/main/java/ru/investbook/api/AbstractRestController.java
+++ b/src/main/java/ru/investbook/api/AbstractRestController.java
@@ -69,7 +69,7 @@ public ResponseEntity get(ID id) {
@Transactional
protected ResponseEntity post(Pojo object) {
try {
- return createAndGet(object)
+ return createAndGetIfAbsent(object)
.map(this::createResponseWithLocationHeader)
.orElseGet(() -> ResponseEntity
.status(HttpStatus.CONFLICT)
@@ -100,7 +100,7 @@ public ResponseEntity put(ID id, Pojo object) {
"запроса [" + objectId + "] не совпадают");
}
Pojo objectWithId = nonNull(objectId) ? object : updateId(id, object);
- return createAndGet(objectWithId)
+ return createAndGetIfAbsent(objectWithId)
.map(this::createResponseWithLocationHeader)
.orElseGet(() -> {
createOrUpdate(objectWithId);
diff --git a/src/main/java/ru/investbook/api/EntityRepositoryService.java b/src/main/java/ru/investbook/api/EntityRepositoryService.java
index 6c51c47c..d0458af8 100644
--- a/src/main/java/ru/investbook/api/EntityRepositoryService.java
+++ b/src/main/java/ru/investbook/api/EntityRepositoryService.java
@@ -35,11 +35,10 @@ public interface EntityRepositoryService {
/**
* Creates a new object with direct INSERT into DB (without prior SELECT call) if possible,
- * calls {@link #create(Object)} otherwise.
+ * calls {@link #createIfAbsent(Object)} otherwise.
*
* @return true if object is created, false if object with ID already exists
- * @throws RuntimeException if an INSERT error occurs
- * @implNote Method performance is the same as {@link #create(Object)} for H2 2.2.224 and MariaDB 11.2
+ * @throws RuntimeException if object not exists and an INSERT error occurs
*/
boolean insert(Pojo object);
@@ -48,21 +47,21 @@ public interface EntityRepositoryService {
* Calls SELECT to check if object's ID exists in DB.
*
* @return true if object was created, false if object with ID already exists
- * @throws RuntimeException if an INSERT error occurs
+ * @throws RuntimeException if object not exists and an INSERT error occurs
* @see #insert(Object)
*/
- boolean create(Pojo object);
+ boolean createIfAbsent(Pojo object);
/**
* Creates new object, doesn't update.
* Calls SELECT to check if object's ID exists in DB.
- * Use faster {@link #create(Object)} method if saved object is not required
+ * Use faster {@link #createIfAbsent(Object)} method if saved object is not required
*
* @return created object or empty Optional if object with ID already exists
- * @throws RuntimeException if an INSERT error occurs
- * @see #create(Object)
+ * @throws RuntimeException if object not exists and an INSERT error occurs
+ * @see #createIfAbsent(Object)
*/
- Optional createAndGet(Pojo object);
+ Optional createAndGetIfAbsent(Pojo object);
/**
* Create new or update existing object in DB.
diff --git a/src/main/java/ru/investbook/parser/InvestbookApiClient.java b/src/main/java/ru/investbook/parser/InvestbookApiClient.java
index e0458bc9..0975f758 100644
--- a/src/main/java/ru/investbook/parser/InvestbookApiClient.java
+++ b/src/main/java/ru/investbook/parser/InvestbookApiClient.java
@@ -54,6 +54,7 @@
import java.util.List;
import java.util.Optional;
+import java.util.function.Consumer;
import java.util.function.Function;
import static org.spacious_team.broker.pojo.CashFlowType.DERIVATIVE_PROFIT;
@@ -78,17 +79,17 @@ public class InvestbookApiClient {
private final ValidatorService validator;
public boolean addPortfolio(Portfolio portfolio) {
- return handlePost(
+ return saveWithoutUpdate(
portfolio,
- portfolioRestController::post,
+ portfolioRestController::createIfAbsent,
"Не могу сохранить Портфель");
}
public void addSecurity(Security security) {
security = convertDerivativeSecurityId(security);
- handlePost(
+ saveWithoutUpdate(
security,
- securityRestController::post,
+ securityRestController::createIfAbsent,
"Не могу добавить ЦБ ");
}
@@ -101,7 +102,7 @@ private Security convertDerivativeSecurityId(Security security) {
}
public void addSecurityDescription(SecurityDescription securityDescription) {
- handlePost(
+ saveWithoutUpdate(
securityDescription,
securityDescriptionRestController::post,
"Не могу добавить метаинформацию о ЦБ ");
@@ -136,23 +137,23 @@ private void addCashTransactionFlows(AbstractTransaction transaction, int transa
}
public boolean addTransaction(Transaction transaction) {
- return handlePost(
+ return saveWithoutUpdate(
transaction,
- transactionRestController::post,
+ transactionRestController::createIfAbsent,
"Не могу добавить транзакцию");
}
public void addTransactionCashFlow(TransactionCashFlow transactionCashFlow) {
- handlePost(
+ saveWithoutUpdate(
transactionCashFlow,
- transactionCashFlowRestController::post,
+ transactionCashFlowRestController::createIfAbsent,
"Не могу добавить информацию о передвижении средств");
}
public void addEventCashFlow(EventCashFlow eventCashFlow) {
- handlePost(
+ saveWithoutUpdate(
eventCashFlow,
- eventCashFlowRestController::post,
+ eventCashFlowRestController::createIfAbsent,
"Не могу добавить информацию о движении денежных средств");
}
@@ -160,65 +161,61 @@ public void addSecurityEventCashFlow(SecurityEventCashFlow cf) {
if (cf.getCount() == null && cf.getEventType() == DERIVATIVE_PROFIT) {
cf = cf.toBuilder().count(0).build(); // count is optional for derivatives
}
- handlePost(
+ saveWithoutUpdate(
cf,
- securityEventCashFlowRestController::post,
+ securityEventCashFlowRestController::createIfAbsent,
"Не могу добавить информацию о движении денежных средств");
}
public void addPortfolioCash(PortfolioCash cash) {
- handlePost(
+ saveWithoutUpdate(
cash,
- portfolioCashRestController::post,
+ portfolioCashRestController::createIfAbsent,
"Не могу добавить информацию об остатках денежных средств портфеля");
}
public void addPortfolioProperty(PortfolioProperty property) {
- handlePost(
+ saveWithoutUpdate(
property,
- portfolioPropertyRestController::post,
+ portfolioPropertyRestController::createIfAbsent,
"Не могу добавить информацию о свойствах портфеля");
}
public void addSecurityQuote(SecurityQuote securityQuote) {
- handlePost(
+ saveWithoutUpdate(
securityQuote,
- securityQuoteRestController::post,
+ securityQuoteRestController::createIfAbsent,
"Не могу добавить информацию о котировке финансового инструмента");
}
public void addForeignExchangeRate(ForeignExchangeRate exchangeRate) {
- handlePost(
+ saveWithoutUpdate(
exchangeRate,
- foreignExchangeRateRestController::post,
+ foreignExchangeRateRestController::createIfAbsent,
"Не могу добавить информацию о курсе валюты");
}
/**
- * @return true if new row was added, or it was already exists in DB, false - or error
+ * @return true - if object was created, or it was already exists in DB,
+ * false - if object not exists and create error was occurred
*/
- // todo replace RestController with EntityRepositoryService
- private boolean handlePost(T object, Function> saver, String errorPrefix) {
+ private boolean saveWithoutUpdate(T object, Consumer persistFunction, String errorMsg) {
try {
validator.validate(object);
- HttpStatusCode status = saver.apply(object).getStatusCode();
- if (!status.is2xxSuccessful() && status != HttpStatus.CONFLICT) {
- log.warn(errorPrefix + " " + object);
- return false;
- }
+ persistFunction.accept(object);
+ return true;
} catch (ConstraintViolationException e) {
- log.warn("{} {}: {}", errorPrefix, object, e.getMessage());
+ log.warn("{} {}: {}", errorMsg, object, e.getMessage());
return false;
} catch (Exception e) {
if (isUniqIndexViolationException(e)) {
- log.debug("Дублирование информации: {} {}", errorPrefix, object);
+ log.debug("Дублирование информации: {} {}", errorMsg, object);
log.trace("Дублирование вызвано исключением", e);
- return true; // same as above status == HttpStatus.CONFLICT
+ return true; // object already exists
} else {
- log.warn("{} {}", errorPrefix, object, e);
+ log.warn("{} {}", errorMsg, object, e);
return false;
}
}
- return true;
}
}
diff --git a/src/test/java/ru/investbook/api/AbstractEntityRepositoryServiceTest.java b/src/test/java/ru/investbook/api/AbstractEntityRepositoryServiceTest.java
index d410acfb..2fa7cff2 100644
--- a/src/test/java/ru/investbook/api/AbstractEntityRepositoryServiceTest.java
+++ b/src/test/java/ru/investbook/api/AbstractEntityRepositoryServiceTest.java
@@ -49,11 +49,11 @@ void insert() {
}
@Test
- void create() {
- test("create()", service::create, false);
- test("create()", service::create, false);
- test("create()", service::create, true);
- test("create()", service::create, true);
+ void createIfAbsent() {
+ test("createIfAbsent()", service::createIfAbsent, false);
+ test("createIfAbsent()", service::createIfAbsent, false);
+ test("createIfAbsent()", service::createIfAbsent, true);
+ test("createIfAbsent()", service::createIfAbsent, true);
}
void test(String name, Consumer consumer, boolean isIdNull) {
From 402621b32099c1cda3584fb23579869169a1e273 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sat, 31 Aug 2024 14:38:50 +0300
Subject: [PATCH 28/40] fix transaction controller POST location header value
"/transactions/null"
---
.../api/AbstractEntityRepositoryService.java | 65 +++++++++++++++++--
.../api/AbstractRestController.java | 16 +++--
.../java/ru/investbook/api/CreateResult.java | 33 ++++++++++
.../api/EntityRepositoryService.java | 11 ++++
.../repository/ConstraintAwareRepository.java | 41 ++++++++++++
.../repository/TransactionRepository.java | 17 ++++-
6 files changed, 170 insertions(+), 13 deletions(-)
create mode 100644 src/main/java/ru/investbook/api/CreateResult.java
create mode 100644 src/main/java/ru/investbook/repository/ConstraintAwareRepository.java
diff --git a/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java b/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java
index 1d8362fc..fc5580f5 100644
--- a/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java
+++ b/src/main/java/ru/investbook/api/AbstractEntityRepositoryService.java
@@ -31,6 +31,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import ru.investbook.converter.EntityConverter;
+import ru.investbook.repository.ConstraintAwareRepository;
import java.util.Optional;
@@ -110,6 +111,11 @@ public Optional createAndGetIfAbsent(Pojo object) {
.map(converter::fromEntity);
}
+ @Override
+ public CreateResult createIfAbsentAndGet(Pojo object) {
+ return createIfAbsentAndGetInternal(object);
+ }
+
/**
* Creates a new object (with SELECT check)
*
@@ -117,17 +123,64 @@ public Optional createAndGetIfAbsent(Pojo object) {
* @implSpec Should be called in transaction
*/
private Optional createIfAbsentInternal(Pojo object) {
- ID id = getId(object);
- if (id != null && existsById(id)) {
- return Optional.empty();
+ Entity entity = null;
+ if (repository instanceof ConstraintAwareRepository caRepository) {
+ entity = converter.toEntity(object);
+ if (caRepository.exists(entity)) {
+ return Optional.empty();
+ }
+ } else {
+ ID id = getId(object);
+ if (id != null && existsById(id)) {
+ return Optional.empty();
+ }
}
- // Если работать не в транзакции, то следующая строка может повторно создать объект.
- // Это возможно, если объект был создан другим потоком после проверки существования строки по ID.
+
+ // Если работать не в транзакции, то следующие строки могут повторно создать объект.
+ // Это возможно, если объект был создан другим потоком после проверки существования строки.
// Метод должен работать в транзакции.
- Entity savedEntity = createOrUpdateInternal(object);
+ if (entity == null) {
+ entity = converter.toEntity(object);
+ }
+ Entity savedEntity = repository.save(entity);
return Optional.of(savedEntity);
}
+ /**
+ * Creates a new object (with SELECT check)
+ *
+ * @return created entity if object is created or existing object otherwise
+ * @implSpec Should be called in transaction
+ */
+ private CreateResult createIfAbsentAndGetInternal(Pojo object) {
+ Entity entity = null;
+ Optional selectedEntity;
+ if (repository instanceof ConstraintAwareRepository caRepository) {
+ entity = converter.toEntity(object);
+ selectedEntity = caRepository.findBy(entity);
+ } else {
+ ID id = getId(object);
+ selectedEntity = Optional.ofNullable(id)
+ .flatMap(repository::findById);
+ }
+ if (selectedEntity.isPresent()) {
+ return selectedEntity
+ .map(converter::fromEntity)
+ .map(CreateResult::selected)
+ .orElseThrow();
+ }
+
+ // Если работать не в транзакции, то следующие строки могут повторно создать объект.
+ // Это возможно, если объект был создан другим потоком после проверки существования строки.
+ // Метод должен работать в транзакции.
+ if (entity == null) {
+ entity = converter.toEntity(object);
+ }
+ Entity savedEntity = repository.save(entity);
+ Pojo savedObject = converter.fromEntity(savedEntity);
+ return CreateResult.created(savedObject);
+ }
+
@Override
@Transactional
public void createOrUpdate(Pojo object) {
diff --git a/src/main/java/ru/investbook/api/AbstractRestController.java b/src/main/java/ru/investbook/api/AbstractRestController.java
index 6c25c9ec..ae71270e 100644
--- a/src/main/java/ru/investbook/api/AbstractRestController.java
+++ b/src/main/java/ru/investbook/api/AbstractRestController.java
@@ -69,12 +69,16 @@ public ResponseEntity get(ID id) {
@Transactional
protected ResponseEntity post(Pojo object) {
try {
- return createAndGetIfAbsent(object)
- .map(this::createResponseWithLocationHeader)
- .orElseGet(() -> ResponseEntity
- .status(HttpStatus.CONFLICT)
- .location(getLocationURI(object))
- .build());
+ CreateResult result = createIfAbsentAndGet(object);
+ Pojo savedObject = result.object();
+ if (result.created()) {
+ return createResponseWithLocationHeader(savedObject);
+ } else {
+ return ResponseEntity
+ .status(HttpStatus.CONFLICT)
+ .location(getLocationURI(savedObject))
+ .build();
+ }
} catch (Exception e) {
throw new InternalServerErrorException("Не могу создать объект", e);
}
diff --git a/src/main/java/ru/investbook/api/CreateResult.java b/src/main/java/ru/investbook/api/CreateResult.java
new file mode 100644
index 00000000..e349c82a
--- /dev/null
+++ b/src/main/java/ru/investbook/api/CreateResult.java
@@ -0,0 +1,33 @@
+/*
+ * InvestBook
+ * Copyright (C) 2024 Spacious Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package ru.investbook.api;
+
+/**
+ * @param created true is new object was created, false if existing object is returned
+ */
+public record CreateResult(T object, boolean created) {
+
+ public static CreateResult created(T object) {
+ return new CreateResult<>(object, true);
+ }
+
+ public static CreateResult selected(T object) {
+ return new CreateResult<>(object, false);
+ }
+}
diff --git a/src/main/java/ru/investbook/api/EntityRepositoryService.java b/src/main/java/ru/investbook/api/EntityRepositoryService.java
index d0458af8..1a802d5c 100644
--- a/src/main/java/ru/investbook/api/EntityRepositoryService.java
+++ b/src/main/java/ru/investbook/api/EntityRepositoryService.java
@@ -63,6 +63,17 @@ public interface EntityRepositoryService {
*/
Optional createAndGetIfAbsent(Pojo object);
+ /**
+ * Creates new object, doesn't update.
+ * Calls SELECT to check if object's ID exists in DB.
+ * Use faster {@link #createIfAbsent(Object)} method if saved object is not required
+ *
+ * @return created object or existing object without update if object with ID already exists
+ * @throws RuntimeException if object not exists and an INSERT error occurs
+ * @see #createIfAbsent(Object)
+ */
+ CreateResult createIfAbsentAndGet(Pojo object);
+
/**
* Create new or update existing object in DB.
* Use instead of the slower method {@link #createOrUpdateAndGet(Object)}
diff --git a/src/main/java/ru/investbook/repository/ConstraintAwareRepository.java b/src/main/java/ru/investbook/repository/ConstraintAwareRepository.java
new file mode 100644
index 00000000..ae3ed9b8
--- /dev/null
+++ b/src/main/java/ru/investbook/repository/ConstraintAwareRepository.java
@@ -0,0 +1,41 @@
+/*
+ * InvestBook
+ * Copyright (C) 2024 Spacious Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package ru.investbook.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.NoRepositoryBean;
+
+import java.util.Optional;
+
+/**
+ * JpaRepository that knows about UNIQUE KEYS
+ */
+@NoRepositoryBean
+public interface ConstraintAwareRepository extends JpaRepository {
+
+ /**
+ * Checks entity existence by UNIQUE KEY fields, other fields is ignored
+ */
+ boolean exists(T probe);
+
+ /**
+ * Selects entity by UNIQUE KEY fields, other fields is ignored
+ */
+ Optional findBy(T probe);
+}
diff --git a/src/main/java/ru/investbook/repository/TransactionRepository.java b/src/main/java/ru/investbook/repository/TransactionRepository.java
index 30528fc0..799e4aca 100644
--- a/src/main/java/ru/investbook/repository/TransactionRepository.java
+++ b/src/main/java/ru/investbook/repository/TransactionRepository.java
@@ -38,7 +38,8 @@
@Transactional(readOnly = true)
public interface TransactionRepository extends
JpaRepository,
- JpaSpecificationExecutor {
+ JpaSpecificationExecutor,
+ ConstraintAwareRepository {
Optional findFirstByOrderByTimestampAsc();
@@ -336,4 +337,18 @@ Collection findByPortfolioAndSecurityIdAndTimestampBetweenDep
@Param("to") Instant toDate);
int countByPortfolioIn(Set portfolio);
+
+ @Override
+ default boolean exists(TransactionEntity probe) {
+ return countByIdOrPortfolioAndTradeId(probe.getId(), probe.getPortfolio(), probe.getTradeId()) > 0;
+ }
+
+ long countByIdOrPortfolioAndTradeId(Integer id, String portfolio, String tradeId);
+
+ @Override
+ default Optional findBy(TransactionEntity probe) {
+ return findByIdOrPortfolioAndTradeId(probe.getId(), probe.getPortfolio(), probe.getTradeId());
+ }
+
+ Optional findByIdOrPortfolioAndTradeId(Integer id, String portfolio, String tradeId);
}
From 709933ec5b5f41f6f40a3ddffbd50a459858ee94 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sun, 1 Sep 2024 20:02:32 +0300
Subject: [PATCH 29/40] make POST response Location header optional
---
.../investbook/api/AbstractRestController.java | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/src/main/java/ru/investbook/api/AbstractRestController.java b/src/main/java/ru/investbook/api/AbstractRestController.java
index ae71270e..03d99d12 100644
--- a/src/main/java/ru/investbook/api/AbstractRestController.java
+++ b/src/main/java/ru/investbook/api/AbstractRestController.java
@@ -25,6 +25,7 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
+import org.springframework.lang.NonNull;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.util.UriUtils;
import ru.investbook.converter.EntityConverter;
@@ -57,7 +58,7 @@ public ResponseEntity get(ID id) {
/**
* Creates a new entity.
- * If entity has ID and record with this ID already exists in DB, "409 Conflict" http status and Location header was returned.
+ * If entity has ID and record with this ID already exists in DB, "409 Conflict" http status and optional Location header was returned.
* Otherwise, "201 Created" http status will be returned with Location header.
* When creating new object, ID may be passed, but that ID only used when no {@link GeneratedValue} set
* on Entity ID field or if {@link GeneratedValue#generator} set to {@link org.hibernate.generator.BeforeExecutionGenerator},
@@ -74,16 +75,23 @@ protected ResponseEntity post(Pojo object) {
if (result.created()) {
return createResponseWithLocationHeader(savedObject);
} else {
- return ResponseEntity
- .status(HttpStatus.CONFLICT)
- .location(getLocationURI(savedObject))
- .build();
+ return createConflictResponse(savedObject);
}
} catch (Exception e) {
throw new InternalServerErrorException("Не могу создать объект", e);
}
}
+ @NonNull
+ private ResponseEntity createConflictResponse(Pojo object) {
+ ResponseEntity.BodyBuilder response = ResponseEntity.status(HttpStatus.CONFLICT);
+ if (getId(object) != null) {
+ URI locationURI = getLocationURI(object);
+ response.location(locationURI);
+ }
+ return response.build();
+ }
+
/**
* Updates or creates a new entity.
* In create case method returns "201 Created" http status and Location header.
From 1cee892b37fe15801896bcb5d94b4ea33d105758 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sun, 1 Sep 2024 20:09:47 +0300
Subject: [PATCH 30/40] improve uniq index violation check for h2 and mariadb
---
.../repository/RepositoryHelper.java | 24 +++++++++++++++++--
1 file changed, 22 insertions(+), 2 deletions(-)
diff --git a/src/main/java/ru/investbook/repository/RepositoryHelper.java b/src/main/java/ru/investbook/repository/RepositoryHelper.java
index 32b25029..7b58d4e5 100644
--- a/src/main/java/ru/investbook/repository/RepositoryHelper.java
+++ b/src/main/java/ru/investbook/repository/RepositoryHelper.java
@@ -20,15 +20,35 @@
import org.hibernate.exception.ConstraintViolationException;
+import java.sql.SQLException;
+import java.util.Objects;
+
public class RepositoryHelper {
/**
- * May return false positive result if NOT NULL column set by NULL
+ * May return false positive result if NOT NULL column set by NULL (or for other constraint violations)
+ * for not H2 or MariaDB RDBMS
*/
public static boolean isUniqIndexViolationException(Throwable t) {
do {
if (t instanceof ConstraintViolationException) {
- return true;
+ // todo Не точное условие, нужно выбирать
+ Throwable cause = t.getCause();
+ if (cause instanceof SQLException sqlException) {
+ int errorCode = sqlException.getErrorCode();
+ String sqlState = sqlException.getSQLState();
+ String packageName = cause.getClass().getPackageName();
+ // https://www.h2database.com/javadoc/org/h2/api/ErrorCode.html#DUPLICATE_KEY_1
+ if (errorCode == 23505 && Objects.equals(packageName, "org.h2.jdbc")) {
+ return true; // H2
+ } else if (errorCode == 1062 &&
+ Objects.equals(sqlState, "23000") &&
+ Objects.equals(packageName, "java.sql")) {
+ // https://mariadb.com/kb/en/mariadb-error-code-reference/
+ return true; // MariaDB
+ }
+ }
+ return true; // other databases
}
} while ((t = t.getCause()) != null);
return false;
From cfedfab1e827f68d5962e3354a45ccf322fb528c Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sun, 1 Sep 2024 20:13:10 +0300
Subject: [PATCH 31/40] replace [insert + select id] with singe select for
duplicate transaction storing stage
---
.../parser/InvestbookApiClient.java | 54 ++++++++++---------
1 file changed, 29 insertions(+), 25 deletions(-)
diff --git a/src/main/java/ru/investbook/parser/InvestbookApiClient.java b/src/main/java/ru/investbook/parser/InvestbookApiClient.java
index 0975f758..c02e8044 100644
--- a/src/main/java/ru/investbook/parser/InvestbookApiClient.java
+++ b/src/main/java/ru/investbook/parser/InvestbookApiClient.java
@@ -34,11 +34,8 @@
import org.spacious_team.broker.pojo.Transaction;
import org.spacious_team.broker.pojo.TransactionCashFlow;
import org.spacious_team.broker.report_parser.api.AbstractTransaction;
-import org.springframework.data.domain.Pageable;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.HttpStatusCode;
-import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
+import ru.investbook.api.CreateResult;
import ru.investbook.api.EventCashFlowRestController;
import ru.investbook.api.ForeignExchangeRateRestController;
import ru.investbook.api.PortfolioCashRestController;
@@ -52,7 +49,6 @@
import ru.investbook.api.TransactionRestController;
import ru.investbook.service.moex.MoexDerivativeCodeService;
-import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -109,23 +105,15 @@ public void addSecurityDescription(SecurityDescription securityDescription) {
}
public void addTransaction(AbstractTransaction transaction) {
- boolean isAdded = addTransaction(transaction.getTransaction());
- if (isAdded) {
- Optional.ofNullable(transaction.getId())
- .or(() -> getSavedTransactionId(transaction))
- .ifPresentOrElse(
- transactionId -> addCashTransactionFlows(transaction, transactionId),
- () -> log.warn("Не могу добавить транзакцию в БД, " +
- "не задан внутренний идентификатор записи: {}", transaction));
- }
- }
-
- private Optional getSavedTransactionId(AbstractTransaction transaction) {
- // todo replace RestController with EntityRepositoryService
- return Optional.of(transactionRestController.get(transaction.getPortfolio(), transaction.getTradeId(), Pageable.unpaged()).getContent())
- .filter(result -> result.size() == 1)
- .map(List::getFirst)
- .map(Transaction::getId);
+ saveWithoutUpdateAndGet(
+ transaction.getTransaction(),
+ transactionRestController::createIfAbsentAndGet,
+ "Не могу добавить транзакцию")
+ .map(Transaction::getId)
+ .ifPresentOrElse(
+ transactionId -> addCashTransactionFlows(transaction, transactionId),
+ () -> log.warn("Не могу добавить транзакцию в БД, " +
+ "не задан внутренний идентификатор записи: {}", transaction));
}
private void addCashTransactionFlows(AbstractTransaction transaction, int transactionId) {
@@ -136,8 +124,8 @@ private void addCashTransactionFlows(AbstractTransaction transaction, int transa
.forEach(this::addTransactionCashFlow);
}
- public boolean addTransaction(Transaction transaction) {
- return saveWithoutUpdate(
+ public void addTransaction(Transaction transaction) {
+ saveWithoutUpdate(
transaction,
transactionRestController::createIfAbsent,
"Не могу добавить транзакцию");
@@ -204,7 +192,7 @@ private boolean saveWithoutUpdate(T object, Consumer persistFunction, Str
validator.validate(object);
persistFunction.accept(object);
return true;
- } catch (ConstraintViolationException e) {
+ } catch (ConstraintViolationException e) { // jakarta.validation, not SQL constraint
log.warn("{} {}: {}", errorMsg, object, e.getMessage());
return false;
} catch (Exception e) {
@@ -218,4 +206,20 @@ private boolean saveWithoutUpdate(T object, Consumer persistFunction, Str
}
}
}
+
+ /**
+ * @return true - if object was created, or it was already exists in DB,
+ * false - if object not exists and create error was occurred
+ */
+ private Optional saveWithoutUpdateAndGet(T object, Function> persistFunction,
+ @SuppressWarnings("SameParameterValue") String errorMsg) {
+ try {
+ validator.validate(object);
+ CreateResult result = persistFunction.apply(object);
+ return Optional.of(result.object());
+ } catch (Exception e) { // jakarta.validation, not SQL constraint
+ log.warn("{} {}: {}", errorMsg, object, e.getMessage());
+ return Optional.empty();
+ }
+ }
}
From 77d5353d01d4dcd466d06acb1a98a1ad4d3eb20d Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sun, 1 Sep 2024 21:07:32 +0300
Subject: [PATCH 32/40] replace [insert + select id] with singe select for
duplicate transaction storing stage
---
src/main/java/ru/investbook/parser/InvestbookApiClient.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/main/java/ru/investbook/parser/InvestbookApiClient.java b/src/main/java/ru/investbook/parser/InvestbookApiClient.java
index c02e8044..2034caa5 100644
--- a/src/main/java/ru/investbook/parser/InvestbookApiClient.java
+++ b/src/main/java/ru/investbook/parser/InvestbookApiClient.java
@@ -217,7 +217,8 @@ private Optional saveWithoutUpdateAndGet(T object, Function result = persistFunction.apply(object);
return Optional.of(result.object());
- } catch (Exception e) { // jakarta.validation, not SQL constraint
+ } catch (Exception e) {
+ // should not be thrown for duplicate
log.warn("{} {}: {}", errorMsg, object, e.getMessage());
return Optional.empty();
}
From 3b257bededc0ad8a2813559030d900a10510d830 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sun, 1 Sep 2024 22:34:57 +0300
Subject: [PATCH 33/40] fix lzy loading by optional=false property
https://stackoverflow.com/questions/17987638/hibernate-one-to-one-lazy-loading-optional-false
---
.../ru/investbook/entity/EventCashFlowEntity.java | 8 ++++----
.../investbook/entity/PortfolioPropertyEntity.java | 4 ++--
.../entity/SecurityEventCashFlowEntity.java | 12 ++++++------
.../ru/investbook/entity/SecurityQuoteEntity.java | 4 ++--
.../investbook/entity/TransactionCashFlowEntity.java | 4 ++--
.../java/ru/investbook/entity/TransactionEntity.java | 9 +++++----
6 files changed, 21 insertions(+), 20 deletions(-)
diff --git a/src/main/java/ru/investbook/entity/EventCashFlowEntity.java b/src/main/java/ru/investbook/entity/EventCashFlowEntity.java
index d3b51737..ae762c1c 100644
--- a/src/main/java/ru/investbook/entity/EventCashFlowEntity.java
+++ b/src/main/java/ru/investbook/entity/EventCashFlowEntity.java
@@ -44,8 +44,8 @@ public class EventCashFlowEntity {
@Column(name = "id")
private Integer id;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "portfolio", referencedColumnName = "id")
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "portfolio", referencedColumnName = "id", nullable = false)
@JsonIgnoreProperties({"hibernateLazyInitializer"})
private PortfolioEntity portfolio;
@@ -53,8 +53,8 @@ public class EventCashFlowEntity {
@Column(name = "timestamp")
private Instant timestamp;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "type", referencedColumnName = "id")
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "type", referencedColumnName = "id", nullable = false)
@JsonIgnoreProperties({"hibernateLazyInitializer"})
private CashFlowTypeEntity cashFlowType;
diff --git a/src/main/java/ru/investbook/entity/PortfolioPropertyEntity.java b/src/main/java/ru/investbook/entity/PortfolioPropertyEntity.java
index 6abf1609..f2cd6aad 100644
--- a/src/main/java/ru/investbook/entity/PortfolioPropertyEntity.java
+++ b/src/main/java/ru/investbook/entity/PortfolioPropertyEntity.java
@@ -41,8 +41,8 @@ public class PortfolioPropertyEntity {
@Column(name = "id")
private Integer id;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "portfolio", referencedColumnName = "id")
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "portfolio", referencedColumnName = "id", nullable = false)
@JsonIgnoreProperties({"hibernateLazyInitializer"})
private PortfolioEntity portfolio;
diff --git a/src/main/java/ru/investbook/entity/SecurityEventCashFlowEntity.java b/src/main/java/ru/investbook/entity/SecurityEventCashFlowEntity.java
index c15df622..fa581868 100644
--- a/src/main/java/ru/investbook/entity/SecurityEventCashFlowEntity.java
+++ b/src/main/java/ru/investbook/entity/SecurityEventCashFlowEntity.java
@@ -44,8 +44,8 @@ public class SecurityEventCashFlowEntity {
@Column(name = "id")
private Integer id;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "portfolio", referencedColumnName = "id")
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "portfolio", referencedColumnName = "id", nullable = false)
@JsonIgnoreProperties({"hibernateLazyInitializer"})
private PortfolioEntity portfolio;
@@ -53,8 +53,8 @@ public class SecurityEventCashFlowEntity {
@Column(name = "timestamp")
private Instant timestamp;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "security", referencedColumnName = "id")
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "security", referencedColumnName = "id", nullable = false)
@JsonIgnoreProperties({"hibernateLazyInitializer"})
private SecurityEntity security;
@@ -62,8 +62,8 @@ public class SecurityEventCashFlowEntity {
@Column(name = "count")
private Integer count;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "type", referencedColumnName = "id")
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "type", referencedColumnName = "id", nullable = false)
@JsonIgnoreProperties({"hibernateLazyInitializer"})
private CashFlowTypeEntity cashFlowType;
diff --git a/src/main/java/ru/investbook/entity/SecurityQuoteEntity.java b/src/main/java/ru/investbook/entity/SecurityQuoteEntity.java
index b3825738..d63a76a9 100644
--- a/src/main/java/ru/investbook/entity/SecurityQuoteEntity.java
+++ b/src/main/java/ru/investbook/entity/SecurityQuoteEntity.java
@@ -42,8 +42,8 @@ public class SecurityQuoteEntity {
@Column(name = "id")
private Integer id;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "security", referencedColumnName = "id")
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "security", referencedColumnName = "id", nullable = false)
@JsonIgnoreProperties({"hibernateLazyInitializer"})
private SecurityEntity security;
diff --git a/src/main/java/ru/investbook/entity/TransactionCashFlowEntity.java b/src/main/java/ru/investbook/entity/TransactionCashFlowEntity.java
index 1aa8e63d..3e931de0 100644
--- a/src/main/java/ru/investbook/entity/TransactionCashFlowEntity.java
+++ b/src/main/java/ru/investbook/entity/TransactionCashFlowEntity.java
@@ -45,7 +45,7 @@ public class TransactionCashFlowEntity {
@Column(name = "transaction_id")
private int transactionId;
- @ManyToOne(fetch = FetchType.LAZY)
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "type", referencedColumnName = "id", nullable = false)
private CashFlowTypeEntity cashFlowType;
@@ -61,7 +61,7 @@ public class TransactionCashFlowEntity {
/*
Nowadays not used, commented due to perf issue
- @ManyToOne(fetch = FetchType.LAZY)
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumns({
@JoinColumn(name = "transaction_id", referencedColumnName = "id", insertable = false, updatable = false),
@JoinColumn(name = "portfolio", referencedColumnName = "portfolio", insertable = false, updatable = false)
diff --git a/src/main/java/ru/investbook/entity/TransactionEntity.java b/src/main/java/ru/investbook/entity/TransactionEntity.java
index f5ea4f83..92c40b3f 100644
--- a/src/main/java/ru/investbook/entity/TransactionEntity.java
+++ b/src/main/java/ru/investbook/entity/TransactionEntity.java
@@ -53,13 +53,14 @@ public class TransactionEntity {
@Column(name = "portfolio")
private String portfolio;
-// @ManyToOne(fetch = FetchType.LAZY)
-// @JoinColumn(name = "portfolio", referencedColumnName = "id")
+// @ManyToOne(fetch = FetchType.LAZY, optional = false)
+// @JoinColumn(name = "portfolio", referencedColumnName = "id", nullable = false)
// @JsonIgnoreProperties({"hibernateLazyInitializer"})
// private PortfolioEntity portfolio;
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "security", referencedColumnName = "id")
+ // https://stackoverflow.com/questions/17987638/hibernate-one-to-one-lazy-loading-optional-false
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "security", referencedColumnName = "id", nullable = false)
@JsonIgnoreProperties({"hibernateLazyInitializer"})
private SecurityEntity security;
From c997a411a670ec2fe68904a5c1df46059f16081a Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sun, 1 Sep 2024 22:47:36 +0300
Subject: [PATCH 34/40] do not do extra select
---
.../ru/investbook/converter/EventCashFlowConverter.java | 6 ++----
.../investbook/converter/PortfolioPropertyConverter.java | 3 +--
.../converter/SecurityDescriptionConverter.java | 3 +--
.../converter/SecurityEventCashFlowConverter.java | 9 +++------
.../ru/investbook/converter/SecurityQuoteConverter.java | 4 +---
.../converter/TransactionCashFlowConverter.java | 6 +-----
.../ru/investbook/converter/TransactionConverter.java | 7 +------
7 files changed, 10 insertions(+), 28 deletions(-)
diff --git a/src/main/java/ru/investbook/converter/EventCashFlowConverter.java b/src/main/java/ru/investbook/converter/EventCashFlowConverter.java
index 7f0a0d39..b0639a67 100644
--- a/src/main/java/ru/investbook/converter/EventCashFlowConverter.java
+++ b/src/main/java/ru/investbook/converter/EventCashFlowConverter.java
@@ -36,10 +36,8 @@ public class EventCashFlowConverter implements EntityConverter new IllegalArgumentException("В справочнике не найден брокерский счет: " + eventCashFlow.getPortfolio()));
- CashFlowTypeEntity cashFlowTypeEntity = cashFlowTypeRepository.findById(eventCashFlow.getEventType().getId())
- .orElseThrow(() -> new IllegalArgumentException("В справочнике не найдено событие с типом: " + eventCashFlow.getEventType().getId()));
+ PortfolioEntity portfolioEntity = portfolioRepository.getReferenceById(eventCashFlow.getPortfolio());
+ CashFlowTypeEntity cashFlowTypeEntity = cashFlowTypeRepository.getReferenceById(eventCashFlow.getEventType().getId());
EventCashFlowEntity entity = new EventCashFlowEntity();
entity.setId(eventCashFlow.getId());
diff --git a/src/main/java/ru/investbook/converter/PortfolioPropertyConverter.java b/src/main/java/ru/investbook/converter/PortfolioPropertyConverter.java
index 6cb85e4c..8ab06baa 100644
--- a/src/main/java/ru/investbook/converter/PortfolioPropertyConverter.java
+++ b/src/main/java/ru/investbook/converter/PortfolioPropertyConverter.java
@@ -33,8 +33,7 @@ public class PortfolioPropertyConverter implements EntityConverter new IllegalArgumentException("В справочнике не найден брокерский счет: " + property.getPortfolio()));
+ PortfolioEntity portfolioEntity = portfolioRepository.getReferenceById(property.getPortfolio());
PortfolioPropertyEntity entity = new PortfolioPropertyEntity();
entity.setId(property.getId());
diff --git a/src/main/java/ru/investbook/converter/SecurityDescriptionConverter.java b/src/main/java/ru/investbook/converter/SecurityDescriptionConverter.java
index 81b76159..e660e6e8 100644
--- a/src/main/java/ru/investbook/converter/SecurityDescriptionConverter.java
+++ b/src/main/java/ru/investbook/converter/SecurityDescriptionConverter.java
@@ -35,8 +35,7 @@ public class SecurityDescriptionConverter implements EntityConverter new IllegalArgumentException("Эмитент c идентификатором не найден: " + security.getIssuer()));
+ issuerEntity = issuerRepository.getReferenceById(security.getIssuer());
}
SecurityDescriptionEntity entity = new SecurityDescriptionEntity();
diff --git a/src/main/java/ru/investbook/converter/SecurityEventCashFlowConverter.java b/src/main/java/ru/investbook/converter/SecurityEventCashFlowConverter.java
index 639dcf9a..6a75f29d 100644
--- a/src/main/java/ru/investbook/converter/SecurityEventCashFlowConverter.java
+++ b/src/main/java/ru/investbook/converter/SecurityEventCashFlowConverter.java
@@ -39,12 +39,9 @@ public class SecurityEventCashFlowConverter implements EntityConverter new IllegalArgumentException("Ценная бумага с заданным ID не найдена: " + eventCashFlow.getSecurity()));
- PortfolioEntity portfolioEntity = portfolioRepository.findById(eventCashFlow.getPortfolio())
- .orElseThrow(() -> new IllegalArgumentException("В справочнике не найден брокерский счет: " + eventCashFlow.getPortfolio()));
- CashFlowTypeEntity cashFlowTypeEntity = cashFlowTypeRepository.findById(eventCashFlow.getEventType().getId())
- .orElseThrow(() -> new IllegalArgumentException("В справочнике не найдено событие с типом: " + eventCashFlow.getEventType().getId()));
+ SecurityEntity securityEntity = securityRepository.getReferenceById(eventCashFlow.getSecurity());
+ PortfolioEntity portfolioEntity = portfolioRepository.getReferenceById(eventCashFlow.getPortfolio());
+ CashFlowTypeEntity cashFlowTypeEntity = cashFlowTypeRepository.getReferenceById(eventCashFlow.getEventType().getId());
SecurityEventCashFlowEntity entity = new SecurityEventCashFlowEntity();
entity.setId(eventCashFlow.getId());
diff --git a/src/main/java/ru/investbook/converter/SecurityQuoteConverter.java b/src/main/java/ru/investbook/converter/SecurityQuoteConverter.java
index 8bac03be..55753fe5 100644
--- a/src/main/java/ru/investbook/converter/SecurityQuoteConverter.java
+++ b/src/main/java/ru/investbook/converter/SecurityQuoteConverter.java
@@ -32,9 +32,7 @@ public class SecurityQuoteConverter implements EntityConverter new IllegalArgumentException("Ценная бумага с заданным ISIN не найдена: " + quote.getSecurity()));
-
+ SecurityEntity securityEntity = securityRepository.getReferenceById(quote.getSecurity());
SecurityQuoteEntity entity = new SecurityQuoteEntity();
entity.setId(quote.getId());
diff --git a/src/main/java/ru/investbook/converter/TransactionCashFlowConverter.java b/src/main/java/ru/investbook/converter/TransactionCashFlowConverter.java
index 799088e3..65ed1e09 100644
--- a/src/main/java/ru/investbook/converter/TransactionCashFlowConverter.java
+++ b/src/main/java/ru/investbook/converter/TransactionCashFlowConverter.java
@@ -35,11 +35,7 @@ public class TransactionCashFlowConverter implements EntityConverter new IllegalArgumentException("В справочнике не найдено событие с типом: " + cash.getEventType().getId()));
+ CashFlowTypeEntity cashFlowTypeEntity = cashFlowTypeRepository.getReferenceById(cash.getEventType().getId());
TransactionCashFlowEntity entity = new TransactionCashFlowEntity();
entity.setId(cash.getId());
diff --git a/src/main/java/ru/investbook/converter/TransactionConverter.java b/src/main/java/ru/investbook/converter/TransactionConverter.java
index 86f34cd5..e502c906 100644
--- a/src/main/java/ru/investbook/converter/TransactionConverter.java
+++ b/src/main/java/ru/investbook/converter/TransactionConverter.java
@@ -23,21 +23,16 @@
import org.springframework.stereotype.Component;
import ru.investbook.entity.SecurityEntity;
import ru.investbook.entity.TransactionEntity;
-import ru.investbook.repository.PortfolioRepository;
import ru.investbook.repository.SecurityRepository;
@Component
@RequiredArgsConstructor
public class TransactionConverter implements EntityConverter {
- private final PortfolioRepository portfolioRepository;
private final SecurityRepository securityRepository;
@Override
public TransactionEntity toEntity(Transaction transaction) {
- portfolioRepository.findById(transaction.getPortfolio())
- .orElseThrow(() -> new IllegalArgumentException("В справочнике не найден брокерский счет: " + transaction.getPortfolio()));
- SecurityEntity securityEntity = securityRepository.findById(transaction.getSecurity())
- .orElseThrow(() -> new IllegalArgumentException("Ценная бумага с заданным ID не найдена: " + transaction.getSecurity()));
+ SecurityEntity securityEntity = securityRepository.getReferenceById(transaction.getSecurity());
TransactionEntity entity = new TransactionEntity();
entity.setId(transaction.getId());
From 478ef69a9d7f457622e786b9bca81a18418a2095 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Sun, 1 Sep 2024 23:54:48 +0300
Subject: [PATCH 35/40] disable mariadb duplicate key errors
---
src/main/resources/application-dev.properties | 1 +
src/main/resources/application-mariadb.properties | 3 ++-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties
index f901f26e..e3f3269d 100644
--- a/src/main/resources/application-dev.properties
+++ b/src/main/resources/application-dev.properties
@@ -22,6 +22,7 @@
#spring.datasource.username = root
#spring.datasource.password = 123456
#spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
+#logging.level.org.mariadb.jdbc.message.server.ErrorPacket=ERROR
# H2
spring.datasource.url=jdbc:h2:file:~/investbook3-test;mode=mysql;non_keywords=value
diff --git a/src/main/resources/application-mariadb.properties b/src/main/resources/application-mariadb.properties
index b5b6be7a..682d3d9b 100644
--- a/src/main/resources/application-mariadb.properties
+++ b/src/main/resources/application-mariadb.properties
@@ -2,4 +2,5 @@
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MariaDBDialect
spring.datasource.username=root
spring.datasource.password=123456
-spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
\ No newline at end of file
+spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
+logging.level.org.mariadb.jdbc.message.server.ErrorPacket=ERROR
From 0b60c4587f3c58dc181811ee94dcda8c4d1baf88 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Mon, 2 Sep 2024 11:22:56 +0300
Subject: [PATCH 36/40] add jpa entity nullable = false fields attribute
---
.../java/ru/investbook/entity/CashFlowTypeEntity.java | 4 ++--
.../java/ru/investbook/entity/EventCashFlowEntity.java | 4 ++--
.../ru/investbook/entity/ForeignExchangeRateEntity.java | 2 +-
.../ru/investbook/entity/ForeignExchangeRateEntityPk.java | 4 ++--
src/main/java/ru/investbook/entity/PortfolioEntity.java | 4 ++--
.../ru/investbook/entity/PortfolioPropertyEntity.java | 6 +++---
.../ru/investbook/entity/SecurityDescriptionEntity.java | 2 +-
.../ru/investbook/entity/SecurityEventCashFlowEntity.java | 8 ++++----
.../java/ru/investbook/entity/SecurityQuoteEntity.java | 4 ++--
.../java/ru/investbook/entity/StockMarketIndexEntity.java | 1 +
.../ru/investbook/entity/TransactionCashFlowEntity.java | 6 +++---
src/main/java/ru/investbook/entity/TransactionEntity.java | 8 ++++----
12 files changed, 27 insertions(+), 26 deletions(-)
diff --git a/src/main/java/ru/investbook/entity/CashFlowTypeEntity.java b/src/main/java/ru/investbook/entity/CashFlowTypeEntity.java
index 61792273..7cc54871 100644
--- a/src/main/java/ru/investbook/entity/CashFlowTypeEntity.java
+++ b/src/main/java/ru/investbook/entity/CashFlowTypeEntity.java
@@ -30,10 +30,10 @@
@Data
public class CashFlowTypeEntity {
@Id
- @Column(name = "id")
+ @Column(name = "id", nullable = false)
private int id;
@Basic
- @Column(name = "name")
+ @Column(name = "name", nullable = false)
private String name;
}
diff --git a/src/main/java/ru/investbook/entity/EventCashFlowEntity.java b/src/main/java/ru/investbook/entity/EventCashFlowEntity.java
index ae762c1c..5301285d 100644
--- a/src/main/java/ru/investbook/entity/EventCashFlowEntity.java
+++ b/src/main/java/ru/investbook/entity/EventCashFlowEntity.java
@@ -59,11 +59,11 @@ public class EventCashFlowEntity {
private CashFlowTypeEntity cashFlowType;
@Basic
- @Column(name = "value")
+ @Column(name = "value", nullable = false)
private BigDecimal value;
@Basic
- @Column(name = "currency")
+ @Column(name = "currency", nullable = false)
private String currency = "RUR";
@Basic
diff --git a/src/main/java/ru/investbook/entity/ForeignExchangeRateEntity.java b/src/main/java/ru/investbook/entity/ForeignExchangeRateEntity.java
index 96989e7e..7949cfd8 100644
--- a/src/main/java/ru/investbook/entity/ForeignExchangeRateEntity.java
+++ b/src/main/java/ru/investbook/entity/ForeignExchangeRateEntity.java
@@ -36,6 +36,6 @@ public class ForeignExchangeRateEntity {
private ForeignExchangeRateEntityPk pk;
@Basic
- @Column(name = "rate")
+ @Column(name = "rate", nullable = false)
private BigDecimal rate;
}
diff --git a/src/main/java/ru/investbook/entity/ForeignExchangeRateEntityPk.java b/src/main/java/ru/investbook/entity/ForeignExchangeRateEntityPk.java
index f1402bae..6057033d 100644
--- a/src/main/java/ru/investbook/entity/ForeignExchangeRateEntityPk.java
+++ b/src/main/java/ru/investbook/entity/ForeignExchangeRateEntityPk.java
@@ -32,10 +32,10 @@
@Data
public class ForeignExchangeRateEntityPk implements Serializable {
- @Column(name = "date")
+ @Column(name = "date", nullable = false)
private LocalDate date;
@Basic
- @Column(name = "currency_pair")
+ @Column(name = "currency_pair", nullable = false)
private String currencyPair;
}
diff --git a/src/main/java/ru/investbook/entity/PortfolioEntity.java b/src/main/java/ru/investbook/entity/PortfolioEntity.java
index 30c01b40..e6b1d91e 100644
--- a/src/main/java/ru/investbook/entity/PortfolioEntity.java
+++ b/src/main/java/ru/investbook/entity/PortfolioEntity.java
@@ -33,10 +33,10 @@
public class PortfolioEntity {
@Id
- @Column(name = "id")
+ @Column(name = "id", nullable = false)
private String id;
@Basic
- @Column(name = "enabled")
+ @Column(name = "enabled", nullable = false)
private boolean enabled;
}
diff --git a/src/main/java/ru/investbook/entity/PortfolioPropertyEntity.java b/src/main/java/ru/investbook/entity/PortfolioPropertyEntity.java
index f2cd6aad..1e94be15 100644
--- a/src/main/java/ru/investbook/entity/PortfolioPropertyEntity.java
+++ b/src/main/java/ru/investbook/entity/PortfolioPropertyEntity.java
@@ -47,14 +47,14 @@ public class PortfolioPropertyEntity {
private PortfolioEntity portfolio;
@Basic
- @Column(name = "timestamp")
+ @Column(name = "timestamp", nullable = false)
private Instant timestamp;
@Basic
- @Column(name = "property")
+ @Column(name = "property", nullable = false)
private String property;
@Basic
- @Column(name = "value")
+ @Column(name = "value", nullable = false)
private String value;
}
diff --git a/src/main/java/ru/investbook/entity/SecurityDescriptionEntity.java b/src/main/java/ru/investbook/entity/SecurityDescriptionEntity.java
index a0638fdf..4fbd73dd 100644
--- a/src/main/java/ru/investbook/entity/SecurityDescriptionEntity.java
+++ b/src/main/java/ru/investbook/entity/SecurityDescriptionEntity.java
@@ -35,7 +35,7 @@
@EqualsAndHashCode(of = "security")
public class SecurityDescriptionEntity {
@Id
- @Column(name = "security")
+ @Column(name = "security", nullable = false)
private int security;
@Column(name = "sector")
diff --git a/src/main/java/ru/investbook/entity/SecurityEventCashFlowEntity.java b/src/main/java/ru/investbook/entity/SecurityEventCashFlowEntity.java
index fa581868..63505f7d 100644
--- a/src/main/java/ru/investbook/entity/SecurityEventCashFlowEntity.java
+++ b/src/main/java/ru/investbook/entity/SecurityEventCashFlowEntity.java
@@ -50,7 +50,7 @@ public class SecurityEventCashFlowEntity {
private PortfolioEntity portfolio;
@Basic
- @Column(name = "timestamp")
+ @Column(name = "timestamp", nullable = false)
private Instant timestamp;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@@ -59,7 +59,7 @@ public class SecurityEventCashFlowEntity {
private SecurityEntity security;
@Basic
- @Column(name = "count")
+ @Column(name = "count", nullable = false)
private Integer count;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@@ -68,10 +68,10 @@ public class SecurityEventCashFlowEntity {
private CashFlowTypeEntity cashFlowType;
@Basic
- @Column(name = "value")
+ @Column(name = "value", nullable = false)
private BigDecimal value;
@Basic
- @Column(name = "currency")
+ @Column(name = "currency", nullable = false)
private String currency = "RUR";
}
diff --git a/src/main/java/ru/investbook/entity/SecurityQuoteEntity.java b/src/main/java/ru/investbook/entity/SecurityQuoteEntity.java
index d63a76a9..1e4038e3 100644
--- a/src/main/java/ru/investbook/entity/SecurityQuoteEntity.java
+++ b/src/main/java/ru/investbook/entity/SecurityQuoteEntity.java
@@ -48,11 +48,11 @@ public class SecurityQuoteEntity {
private SecurityEntity security;
@Basic
- @Column(name = "timestamp")
+ @Column(name = "timestamp", nullable = false)
private Instant timestamp;
@Basic
- @Column(name = "quote")
+ @Column(name = "quote", nullable = false)
private BigDecimal quote;
@Basic
diff --git a/src/main/java/ru/investbook/entity/StockMarketIndexEntity.java b/src/main/java/ru/investbook/entity/StockMarketIndexEntity.java
index afff6308..c8d08143 100644
--- a/src/main/java/ru/investbook/entity/StockMarketIndexEntity.java
+++ b/src/main/java/ru/investbook/entity/StockMarketIndexEntity.java
@@ -41,6 +41,7 @@
public class StockMarketIndexEntity {
@Id
+ @Column(name = "date", nullable = false)
private LocalDate date;
@Basic
diff --git a/src/main/java/ru/investbook/entity/TransactionCashFlowEntity.java b/src/main/java/ru/investbook/entity/TransactionCashFlowEntity.java
index 3e931de0..73639849 100644
--- a/src/main/java/ru/investbook/entity/TransactionCashFlowEntity.java
+++ b/src/main/java/ru/investbook/entity/TransactionCashFlowEntity.java
@@ -42,7 +42,7 @@ public class TransactionCashFlowEntity {
private Integer id;
@Basic(optional = false)
- @Column(name = "transaction_id")
+ @Column(name = "transaction_id", nullable = false)
private int transactionId;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@@ -50,11 +50,11 @@ public class TransactionCashFlowEntity {
private CashFlowTypeEntity cashFlowType;
@Basic(optional = false)
- @Column(name = "value")
+ @Column(name = "value", nullable = false)
private BigDecimal value;
@Basic(optional = false)
- @Column(name = "currency")
+ @Column(name = "currency", nullable = false)
private String currency = "RUR";
diff --git a/src/main/java/ru/investbook/entity/TransactionEntity.java b/src/main/java/ru/investbook/entity/TransactionEntity.java
index 92c40b3f..c2f54c88 100644
--- a/src/main/java/ru/investbook/entity/TransactionEntity.java
+++ b/src/main/java/ru/investbook/entity/TransactionEntity.java
@@ -46,11 +46,11 @@ public class TransactionEntity {
private Integer id;
@Basic(optional = false)
- @Column(name = "trade_id")
+ @Column(name = "trade_id", nullable = false)
private String tradeId;
@Basic(optional = false)
- @Column(name = "portfolio")
+ @Column(name = "portfolio", nullable = false)
private String portfolio;
// @ManyToOne(fetch = FetchType.LAZY, optional = false)
@@ -65,11 +65,11 @@ public class TransactionEntity {
private SecurityEntity security;
@Basic(optional = false)
- @Column(name = "timestamp")
+ @Column(name = "timestamp", nullable = false)
private Instant timestamp;
@Basic(optional = false)
- @Column(name = "count")
+ @Column(name = "count", nullable = false)
private int count;
/*
From 96c907d6362e566c1f6e2ed90289091a91f5b705 Mon Sep 17 00:00:00 2001
From: vananiev
Date: Mon, 22 Jul 2024 01:20:08 +0300
Subject: [PATCH 37/40] add openapi response codes
---
pom.xml | 2 +-
.../api/CashFlowTypeRestController.java | 18 +++++--
.../api/EventCashFlowRestController.java | 31 +++++++++---
.../ForeignExchangeRateRestController.java | 47 ++++++++++++++-----
.../investbook/api/IssuerRestController.java | 33 ++++++++++---
.../api/PortfolioCashRestController.java | 31 +++++++++---
.../api/PortfolioPropertyRestController.java | 36 +++++++++++---
.../api/PortfolioRestController.java | 31 +++++++++---
.../SecurityDescriptionRestController.java | 37 +++++++++++----
.../SecurityEventCashFlowRestController.java | 34 ++++++++++----
.../api/SecurityQuoteRestController.java | 33 ++++++++++---
.../api/SecurityRestController.java | 37 +++++++++++----
.../TransactionCashFlowRestController.java | 36 +++++++++++---
.../api/TransactionRestController.java | 36 +++++++++++---
.../resources/application-core.properties | 3 +-
15 files changed, 346 insertions(+), 99 deletions(-)
diff --git a/pom.xml b/pom.xml
index 748f2853..12f498fe 100644
--- a/pom.xml
+++ b/pom.xml
@@ -189,7 +189,7 @@
org.springdoc
springdoc-openapi-starter-webmvc-ui
- 2.1.0
+ 2.6.0
com.sun.mail
diff --git a/src/main/java/ru/investbook/api/CashFlowTypeRestController.java b/src/main/java/ru/investbook/api/CashFlowTypeRestController.java
index e454cc63..f70db7c6 100644
--- a/src/main/java/ru/investbook/api/CashFlowTypeRestController.java
+++ b/src/main/java/ru/investbook/api/CashFlowTypeRestController.java
@@ -20,6 +20,10 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.spacious_team.broker.pojo.CashFlowType;
@@ -32,7 +36,6 @@
import ru.investbook.repository.CashFlowTypeRepository;
import java.util.Optional;
-import java.util.stream.Collectors;
@RestController
@RequiredArgsConstructor
@@ -44,16 +47,23 @@ public class CashFlowTypeRestController {
private final CashFlowTypeConverter cashFlowTypeConverter;
@GetMapping
- @Operation(summary = "Отобразить все")
+ @Operation(summary = "Отобразить все",
+ responses = {
+ @ApiResponse(responseCode = "200", content = @Content(
+ array = @ArraySchema(schema = @Schema(implementation = CashFlowType.class)))),
+ @ApiResponse(responseCode = "500", content = @Content)})
public Iterable getCashFlowType() {
return cashFlowTypeRepository.findAll()
.stream()
.map(cashFlowTypeConverter::fromEntity)
- .collect(Collectors.toList());
+ .toList();
}
@GetMapping("{id}")
- @Operation(summary = "Отобразить по идентификатору")
+ @Operation(summary = "Отобразить по идентификатору",
+ responses = {
+ @ApiResponse(responseCode = "200"),
+ @ApiResponse(responseCode = "500", content = @Content)})
public ResponseEntity