feat: add fetch size, max-rows, timeout settings

This commit is contained in:
Sergey Krylov 2026-03-05 05:29:10 +03:00
parent 168e9a99c7
commit e868169f3d
13 changed files with 457 additions and 107 deletions

View File

@ -2,3 +2,6 @@ app.datasource.url=jdbc:postgresql://localhost:5434/charm_repository
app.datasource.username=postgres
app.datasource.password=123456
app.datasource.driver=org.postgresql.Driver
app.datasource.fetch-size=50
app.datasource.max-rows=100
app.datasource.query-timeout=5

View File

@ -10,8 +10,10 @@ import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import ru.charm.back.dto.ProfileFilter;
import ru.charm.back.dto.ProfileGetDto;
import ru.charm.back.dto.ProfileUpdateDto;
import ru.charm.back.mapper.RequestToProfileFilterMapper;
import ru.charm.back.mapper.RequestToProfileUpdateDtoMapper;
import ru.charm.back.service.ProfileService;
@ -27,6 +29,8 @@ public class ProfileController extends HttpServlet {
private final ProfileService service = ProfileService.getInstance();
private final RequestToProfileUpdateDtoMapper requestToProfileUpdateDtoMapper = RequestToProfileUpdateDtoMapper.getInstance();
private final RequestToProfileFilterMapper requestToProfileFilterMapper = RequestToProfileFilterMapper.getInstance();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
@ -66,7 +70,8 @@ public class ProfileController extends HttpServlet {
resp.sendError(SC_NOT_FOUND);
}
} else {
req.setAttribute("profiles", service.findAll());
ProfileFilter filter = requestToProfileFilterMapper.map(req);
req.setAttribute("profiles", service.findAll(filter));
req.getRequestDispatcher("/WEB-INF/jsp/profiles.jsp").forward(req, resp);
}
}

View File

@ -9,7 +9,9 @@ import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import ru.charm.back.dto.ProfileFilter;
import ru.charm.back.dto.ProfileGetDto;
import ru.charm.back.mapper.RequestToProfileFilterMapper;
import ru.charm.back.service.ProfileService;
import java.io.IOException;
@ -23,6 +25,8 @@ import static ru.charm.back.utils.UrlUtils.REST_URL;
@MultipartConfig
public class ProfileController extends HttpServlet {
private final ProfileService service = ProfileService.getInstance();
private final RequestToProfileFilterMapper requestToProfileFilterMapper = RequestToProfileFilterMapper.getInstance();
private final ObjectMapper objectMapper = JsonMapper.builder()
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true)
@ -45,7 +49,8 @@ public class ProfileController extends HttpServlet {
resp.sendError(SC_NOT_FOUND);
}
} else {
objectMapper.writeValue(writer, service.findAll());
ProfileFilter filter = requestToProfileFilterMapper.map(req);
objectMapper.writeValue(writer, service.findAll(filter));
}
}
}

View File

@ -4,38 +4,39 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import ru.charm.back.dto.ProfileFilter;
import ru.charm.back.dto.ProfileSelectQueryBuilder;
import ru.charm.back.dto.ProfileUpdateQueryBuilder;
import ru.charm.back.dto.Query;
import ru.charm.back.model.Gender;
import ru.charm.back.model.Profile;
import ru.charm.back.model.Role;
import ru.charm.back.model.Status;
import ru.charm.back.utils.ConfigFile;
import ru.charm.back.utils.ConfigFileUtils;
import ru.charm.back.utils.ConnectionManager;
import java.sql.*;
import java.sql.Date;
import java.util.*;
import static ru.charm.back.utils.ConnectionManager.getConnection;
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ProfileDao {
public static final String INSERT = "INSERT INTO profile (email, password) VALUES (?, ?)";
private static final ProfileDao INSTANCE = new ProfileDao();
public static final String URL = ConfigFile.get("app.datasource.url");
public static final String USER = ConfigFile.get("app.datasource.username");
public static final String PASSWORD = ConfigFile.get("app.datasource.password");
public static final String DRIVER = ConfigFile.get("app.datasource.driver");
@SneakyThrows
public static ProfileDao getInstance() {
Class.forName(DRIVER);
return INSTANCE;
}
public Profile save(Profile profile) {
String sql = "INSERT INTO profile(email, password) VALUES (?, ?)";
try (
Connection connection = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
Connection connection = getConnection();
PreparedStatement statement = connection.prepareStatement(INSERT, Statement.RETURN_GENERATED_KEYS);
) {
statement.setString(1, profile.getEmail());
@ -54,13 +55,15 @@ public class ProfileDao {
}
}
public Optional<Profile> findById(Long id) {
Query query = new ProfileSelectQueryBuilder().addIdFilter(id).build();
try (
Connection connection = DriverManager.getConnection(URL, USER, PASSWORD);
Statement statement = connection.createStatement();
Connection connection = getConnection();
PreparedStatement statement = ConnectionManager.getPreparedStmt(connection, query);
) {
String sql = "SELECT * FROM profile WHERE id = '%s'".formatted(id);
ResultSet res = statement.executeQuery(sql);
ResultSet res = statement.executeQuery();
Profile profile = null;
if (res.next()) {
@ -74,13 +77,24 @@ public class ProfileDao {
}
}
public List<Profile> findAll() {
public List<Profile> findAll(ProfileFilter filter) {
Query query = new ProfileSelectQueryBuilder()
.addEmailStartWithFilter(filter.getEmailStartWith())
.addNameStartWith(filter.getNameStartWith())
.addSurnameStartWith(filter.getSurnameStartWith())
.addStatus(filter.getStatus())
.addLTAge(filter.getLtAge())
.addGTEAge(filter.getGteAge())
.build();
try (
Connection connection = DriverManager.getConnection(URL, USER, PASSWORD);
Statement statement = connection.createStatement();
Connection connection = ConnectionManager.getConnection();
PreparedStatement statement = ConnectionManager.getPreparedStmt(connection, query);
) {
String sql = "SELECT * FROM profile" ;
ResultSet res = statement.executeQuery(sql);
statement.setFetchSize(ConnectionManager.FETCH_SIZE);
statement.setMaxRows(ConnectionManager.MAX_ROWS);
statement.setQueryTimeout(ConnectionManager.QUERY_TIMEOUT);
ResultSet res = statement.executeQuery();
List<Profile> profiles = new ArrayList<>();
while (res.next()) {
@ -94,81 +108,32 @@ public class ProfileDao {
}
}
public Optional<Profile> findByEmailAndPassword(String email, String password) {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
Statement stmt = conn.createStatement()) {
//language=POSTGRES-PSQL
String sql = "SELECT * FROM profile WHERE email = '%s' AND password = '%s'";
ResultSet rs = stmt.executeQuery(String.format(sql, email, password));
Profile profile = null;
if (rs.next()) {
profile = mapToProfile(rs);
}
return Optional.ofNullable(profile);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void update(Profile profile) {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
Statement stmt = conn.createStatement()) {
List<Object> args = new ArrayList<>();
StringBuilder queryBuilder = new StringBuilder("UPDATE profile SET email = '%s', password = '%s'");
args.add(profile.getEmail());
args.add(profile.getPassword());
Query query = new ProfileUpdateQueryBuilder()
.addEmail(profile.getEmail())
.addPassword(profile.getPassword())
.addName(profile.getName())
.addSurname(profile.getSurname())
.addBirthDate(profile.getBirthDate())
.addAbout(profile.getAbout())
.addGender(profile.getGender())
.addStatus(profile.getStatus())
.addPhoto(profile.getPhoto())
.build(profile.getId());
if (profile.getName() != null) {
queryBuilder.append(", name = '%s'");
args.add(profile.getName());
}
if (profile.getSurname() != null) {
queryBuilder.append(", surname = '%s'");
args.add(profile.getSurname());
}
if (profile.getBirthDate() != null) {
queryBuilder.append(", birth_date = '%s'");
args.add(Date.valueOf(profile.getBirthDate()));
}
if (profile.getAbout() != null) {
queryBuilder.append(", about = '%s'");
args.add(profile.getAbout());
}
if (profile.getGender() != null) {
queryBuilder.append(", gender = '%s'");
args.add(profile.getGender());
}
if (profile.getStatus() != null) {
queryBuilder.append(", status = '%s'");
args.add(profile.getStatus());
}
if (profile.getPhoto() != null) {
queryBuilder.append(", photo = '%s'");
args.add(profile.getPhoto());
}
queryBuilder.append(" WHERE id = %s");
args.add(profile.getId());
String sql = queryBuilder.toString().formatted(args.toArray());
log.debug("Final update sql: {}", sql);
int updateCount = stmt.executeUpdate(sql);
log.debug("Update count: {}", updateCount);
try (Connection conn = ConnectionManager.getConnection();
PreparedStatement stmt = ConnectionManager.getPreparedStmt(conn, query)) {
stmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public boolean delete(Long id) {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
Statement stmt = conn.createStatement()) {
//language=POSTGRES-PSQL
String sql = "DELETE FROM profile WHERE id = %s".formatted(id);
int deleteCount = stmt.executeUpdate(sql);
String sql = "DELETE FROM profile WHERE id = ?";
try (Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
int deleteCount = stmt.executeUpdate();
log.debug("Delete count: {}", deleteCount);
return deleteCount > 0;
} catch (SQLException e) {
@ -178,7 +143,7 @@ public class ProfileDao {
public Set<String> getAllEmails() {
try (
Connection connection = DriverManager.getConnection(URL, USER, PASSWORD);
Connection connection = getConnection();
Statement statement = connection.createStatement();
) {
String sql = "SELECT email FROM profile" ;
@ -197,11 +162,11 @@ public class ProfileDao {
}
public Optional<Profile> findByEmail(String email) {
String sql = "SELECT * FROM profile WHERE email = ?";
Query query = new ProfileSelectQueryBuilder().addEmailFilter(email).build();
try (
Connection connection = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement statement = connection.prepareStatement(sql);
Connection connection = ConnectionManager.getConnection();
PreparedStatement statement = ConnectionManager.getPreparedStmt(connection, query);
) {
statement.setString(1, email);
ResultSet res = statement.executeQuery();
@ -219,12 +184,13 @@ public class ProfileDao {
}
public Boolean existsByEmail(String email) {
Query query = new ProfileSelectQueryBuilder().addEmailFilter(email).build();
try (
Connection connection = DriverManager.getConnection(URL, USER, PASSWORD);
Statement statement = connection.createStatement();
Connection connection = ConnectionManager.getConnection();
PreparedStatement statement = ConnectionManager.getPreparedStmt(connection, query);
) {
String sql = "SELECT * FROM profile WHERE email = '%s'".formatted(email);
ResultSet res = statement.executeQuery(sql);
ResultSet res = statement.executeQuery();
return res.next();

View File

@ -0,0 +1,17 @@
package ru.charm.back.dto;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import ru.charm.back.model.Status;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class ProfileFilter {
String emailStartWith;
String nameStartWith;
String surnameStartWith;
Integer ltAge;
Integer gteAge;
Status status;
}

View File

@ -0,0 +1,115 @@
package ru.charm.back.dto;
import ru.charm.back.model.Status;
import java.sql.Date;
import java.util.ArrayList;
import java.util.List;
import static ru.charm.back.utils.DateTimeUtils.getPastDate;
public class ProfileSelectQueryBuilder {
//language=POSTGRES-PSQL
public static final String SELECT_BASE = """
SELECT id, email, password, "name", surname, birth_date, about, gender, photo, status, role
FROM profile
WHERE '' = ''
""";
private final StringBuilder sb;
private final List<Object> args;
public ProfileSelectQueryBuilder() {
this.sb = new StringBuilder(SELECT_BASE);
this.args = new ArrayList<>();
}
public ProfileSelectQueryBuilder addIdFilter(Long id) {
if (id == null) {
return this;
}
sb.append(" AND id = ?");
args.add(id);
return this;
}
public ProfileSelectQueryBuilder addEmailFilter(String email) {
if (email == null) {
return this;
}
sb.append(" AND email = ?");
args.add(email);
return this;
}
public ProfileSelectQueryBuilder addPasswordFilter(String password) {
if (password == null) {
return this;
}
sb.append(" AND password = ?");
args.add(password);
return this;
}
public ProfileSelectQueryBuilder addEmailStartWithFilter(String emailStartWith) {
if (emailStartWith == null) {
return this;
}
sb.append(" AND email like ?");
args.add(emailStartWith + "%");
return this;
}
public ProfileSelectQueryBuilder addNameStartWith(String nameStartWith) {
if (nameStartWith == null) {
return this;
}
sb.append(" AND name like ?");
args.add(nameStartWith + "%");
return this;
}
public ProfileSelectQueryBuilder addSurnameStartWith(String surnameStartWith) {
if (surnameStartWith == null) {
return this;
}
sb.append(" AND surname like ?");
args.add(surnameStartWith + "%");
return this;
}
public ProfileSelectQueryBuilder addLTAge(Integer ltAge) {
if (ltAge == null) {
return this;
}
Date gtDate = getPastDate(ltAge);
sb.append(" AND birth_date > ?");
args.add(gtDate);
return this;
}
public ProfileSelectQueryBuilder addGTEAge(Integer gteAge) {
if (gteAge == null) {
return this;
}
Date lteDate = getPastDate(gteAge);
sb.append(" AND birth_date <= ?");
args.add(lteDate);
return this;
}
public ProfileSelectQueryBuilder addStatus(Status status) {
if (status == null) {
return this;
}
sb.append(" AND status = ?");
args.add(status.toString());
return this;
}
public Query build() {
return new Query(sb.toString(), args);
}
}

View File

@ -0,0 +1,109 @@
package ru.charm.back.dto;
import ru.charm.back.model.Gender;
import ru.charm.back.model.Status;
import java.sql.Date;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
public class ProfileUpdateQueryBuilder {
//language=POSTGRES-PSQL
public static final String UPDATE_BASE = "UPDATE profile SET id = id";
private final StringBuilder sb;
private final List<Object> args;
public ProfileUpdateQueryBuilder() {
this.sb = new StringBuilder(UPDATE_BASE);
this.args = new ArrayList<>();
}
public ProfileUpdateQueryBuilder addEmail(String email) {
if (email == null) {
return this;
}
sb.append(", email = ?");
args.add(email);
return this;
}
public ProfileUpdateQueryBuilder addPassword(String password) {
if (password == null) {
return this;
}
sb.append(", password = ?");
args.add(password);
return this;
}
public ProfileUpdateQueryBuilder addName(String name) {
if (name == null) {
return this;
}
sb.append(", name = ?");
args.add(name);
return this;
}
public ProfileUpdateQueryBuilder addSurname(String surname) {
if (surname == null) {
return this;
}
sb.append(", surname = ?");
args.add(surname);
return this;
}
public ProfileUpdateQueryBuilder addBirthDate(LocalDate birthDate) {
if (birthDate == null) {
return this;
}
sb.append(", birth_date = ?");
args.add(Date.valueOf(birthDate));
return this;
}
public ProfileUpdateQueryBuilder addAbout(String about) {
if (about == null) {
return this;
}
sb.append(", about = ?");
args.add(about);
return this;
}
public ProfileUpdateQueryBuilder addGender(Gender gender) {
if (gender == null) {
return this;
}
sb.append(", gender = ?");
args.add(gender.toString());
return this;
}
public ProfileUpdateQueryBuilder addStatus(Status status) {
if (status == null) {
return this;
}
sb.append(", status = ?");
args.add(status.toString());
return this;
}
public ProfileUpdateQueryBuilder addPhoto(String photo) {
if (photo == null) {
return this;
}
sb.append(", photo = ?");
args.add(photo);
return this;
}
public Query build(Long id) {
sb.append(" WHERE id = ?");
args.add(id);
return new Query(sb.toString(), args);
}
}

View File

@ -0,0 +1,6 @@
package ru.charm.back.dto;
import java.util.List;
public record Query(String sql, List<Object> args) {
}

View File

@ -0,0 +1,54 @@
package ru.charm.back.mapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import ru.charm.back.dto.ProfileFilter;
import ru.charm.back.model.Status;
import static ru.charm.back.utils.StringUtils.isBlank;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class RequestToProfileFilterMapper implements Mapper<HttpServletRequest, ProfileFilter> {
private static final RequestToProfileFilterMapper INSTANCE = new RequestToProfileFilterMapper();
public static RequestToProfileFilterMapper getInstance() {
return INSTANCE;
}
@Override
public ProfileFilter map(HttpServletRequest req) {
return map(req, new ProfileFilter());
}
@Override
public ProfileFilter map(HttpServletRequest req, ProfileFilter filter) {
String emailStartWithArg = req.getParameter("emailStartWith");
String emailStartWith = isBlank(emailStartWithArg) ? null : emailStartWithArg;
filter.setEmailStartWith(emailStartWith);
String nameStartWithArg = req.getParameter("nameStartWith");
String nameStartWith = isBlank(nameStartWithArg) ? null : nameStartWithArg;
filter.setNameStartWith(nameStartWith);
String surnameStartWithArg = req.getParameter("surnameStartWith");
String surnameStartWith = isBlank(surnameStartWithArg) ? null : surnameStartWithArg;
filter.setSurnameStartWith(surnameStartWith);
String ltAgeArg = req.getParameter("ltAge");
Integer ltAge = isBlank(ltAgeArg) ? null : Integer.parseInt(ltAgeArg);
filter.setLtAge(ltAge);
String gteAgeArg = req.getParameter("gteAge");
Integer gteAge = isBlank(gteAgeArg) ? null : Integer.parseInt(gteAgeArg);
filter.setGteAge(gteAge);
String statusArg = req.getParameter("status");
Status status = isBlank(statusArg) ? null : Status.valueOf(statusArg);
filter.setStatus(status);
return filter;
}
}

View File

@ -4,10 +4,7 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import ru.charm.back.dao.ProfileDao;
import ru.charm.back.dto.LoginDto;
import ru.charm.back.dto.ProfileGetDto;
import ru.charm.back.dto.ProfileUpdateDto;
import ru.charm.back.dto.RegistrationDto;
import ru.charm.back.dto.*;
import ru.charm.back.mapper.ProfileToProfileGetDtoMapper;
import ru.charm.back.mapper.ProfileUpdateDtoToProfileMapper;
import ru.charm.back.mapper.RegistrationDtoToProfileMapper;
@ -46,8 +43,8 @@ public class ProfileService {
return dao.findById(id).map(profileToProfileGetDtoMapper::map);
}
public List<ProfileGetDto> findAll() {
return dao.findAll().stream().map(profileToProfileGetDtoMapper::map).toList();
public List<ProfileGetDto> findAll(ProfileFilter filter) {
return dao.findAll(filter).stream().map(profileToProfileGetDtoMapper::map).toList();
}
@SneakyThrows

View File

@ -7,7 +7,7 @@ import java.io.InputStream;
import java.util.Properties;
@UtilityClass
public class ConfigFile {
public class ConfigFileUtils {
private static final Properties CONFIG = new Properties();
static {
@ -20,7 +20,7 @@ public class ConfigFile {
private static void loadConfig() {
try (InputStream resourceAsStream = ConfigFile.class.getClassLoader().getResourceAsStream("application.properties")) {
try (InputStream resourceAsStream = ConfigFileUtils.class.getClassLoader().getResourceAsStream("application.properties")) {
CONFIG.load(resourceAsStream);
} catch (IOException e) {
throw new RuntimeException(e);

View File

@ -0,0 +1,48 @@
package ru.charm.back.utils;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import ru.charm.back.dto.Query;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
@UtilityClass
public class ConnectionManager {
public static final String URL = ConfigFileUtils.get("app.datasource.url");
public static final String USER = ConfigFileUtils.get("app.datasource.username");
public static final String PASSWORD = ConfigFileUtils.get("app.datasource.password");
public static final String DRIVER = ConfigFileUtils.get("app.datasource.driver");
private static final String FETCH_SIZE_STR = ConfigFileUtils.get("app.datasource.fetch-size");
public static final int FETCH_SIZE = Integer.parseInt(FETCH_SIZE_STR != null ? FETCH_SIZE_STR : "100");
private static final String MAX_ROWS_STR = ConfigFileUtils.get("app.datasource.max-rows");
public static final int MAX_ROWS = Integer.parseInt(MAX_ROWS_STR != null ? MAX_ROWS_STR : "1000");
private static final String QUERY_TIMEOUT_STR = ConfigFileUtils.get("app.datasource.query-timeout");
public static final int QUERY_TIMEOUT = Integer.parseInt(QUERY_TIMEOUT_STR != null ? QUERY_TIMEOUT_STR : "10");
static {
if (DRIVER != null) {
try {
Class.forName(DRIVER);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
public static PreparedStatement getPreparedStmt(Connection conn, Query query) throws SQLException {
PreparedStatement stmt = conn.prepareStatement(query.sql());
List<Object> args = query.args();
for (int i = 0; i < args.size(); i++) {
stmt.setObject(i + 1, args.get(i));
}
return stmt;
}
}

View File

@ -0,0 +1,25 @@
package ru.charm.back.utils;
import lombok.experimental.UtilityClass;
import java.sql.Date;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
@UtilityClass
public class DateTimeUtils {
public static int getAge(LocalDate birthDate) {
return Math.toIntExact(ChronoUnit.YEARS.between(birthDate, LocalDate.now()));
}
public static Date getPastDate(int age) {
return Date.valueOf(LocalDate.now().minusYears(age));
}
public static boolean isValidAge(LocalDate birthDate) {
if (birthDate == null) return false;
int age = getAge(birthDate);
return age > 18 && age < 100;
}
}