diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..28d595e
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,93 @@
+
+ 4.0.0
+
+ com.example
+ my-project
+ 1.0-SNAPSHOT
+
+
+ 8
+ 8
+
+
+
+
+
+ mysql
+ mysql-connector-java
+ 8.0.30
+
+
+
+ org.hibernate
+ hibernate-core-jakarta
+ 5.6.14.Final
+
+
+
+ p6spy
+ p6spy
+ 3.9.1
+
+
+
+ io.lettuce
+ lettuce-core
+ 6.2.2.RELEASE
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+ provided
+
+
+
+ org.mockito
+ mockito-core
+ 5.12.0
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.12.0
+ test
+
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.13
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.14.0
+
+
+ redis.clients
+ jedis
+ 5.2.0
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 21
+ 21
+
+
+
+
+
diff --git a/src/main/java/com/javarush/Application.java b/src/main/java/com/javarush/Application.java
new file mode 100644
index 0000000..56c48c5
--- /dev/null
+++ b/src/main/java/com/javarush/Application.java
@@ -0,0 +1,124 @@
+package com.javarush;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.javarush.cache.RedisRepository;
+import com.javarush.config.HibernateUtil;
+import com.javarush.config.RedisConfig;
+import com.javarush.domain.entity.City;
+import com.javarush.domain.entity.CountryLanguage;
+import com.javarush.redis.CityCountry;
+import com.javarush.repository.CityRepository;
+import com.javarush.repository.CountryRepository;
+import com.javarush.services.CityService;
+import com.javarush.services.CountryService;
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.api.StatefulRedisConnection;
+import io.lettuce.core.api.sync.RedisStringCommands;
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import static java.util.Objects.nonNull;
+
+public class Application {
+ public final SessionFactory sessionFactory;
+ public final RedisClient redisClient;
+ public final CityService cityService;
+ public final CountryService countryService;
+ public final ObjectMapper mapper;
+ private static final Logger LOGGER = LoggerFactory.getLogger(Application.class);
+
+ public Application() {
+ sessionFactory = HibernateUtil.getSessionFactory();
+ redisClient = RedisConfig.prepareRedisClient();
+ cityService = new CityService(new RedisRepository(), new CityRepository());
+ countryService = new CountryService(new RedisRepository(), new CountryRepository());
+ mapper = new ObjectMapper();
+ }
+
+ private void shutdown() {
+ if (nonNull(sessionFactory)) {
+ sessionFactory.close();
+ }
+ if (nonNull(redisClient)) {
+ redisClient.shutdown();
+ }
+ }
+
+ public static void main(String[] args) {
+ int numOfQueries = 15;
+ Application application = new Application();
+ RedisRepository countryRedisRepository = new RedisRepository();
+ RedisRepository cityRedisRepository = new RedisRepository();
+
+ System.out.println("Querying Country by ID...");
+ for (int i = 0; i < numOfQueries; i++) {
+ application.countryService.getById(1);
+ application.cityService.getById(2);
+ }
+ }
+
+ private void pushToRedis(List data) {
+ try (StatefulRedisConnection connection = redisClient.connect()) {
+ RedisStringCommands sync = connection.sync();
+ for (CityCountry cityCountry : data) {
+ try {
+ sync.set(String.valueOf(cityCountry.getId()), mapper.writeValueAsString(cityCountry));
+ } catch (JsonProcessingException e) {
+ LOGGER.error("Couldn't push to redis : ", e);
+ e.printStackTrace(System.out);
+ }
+ }
+
+ }
+ }
+
+ private void testRedisData(List ids) {
+ try (StatefulRedisConnection connection = redisClient.connect()) {
+ RedisStringCommands sync = connection.sync();
+ for (Integer id : ids) {
+ String value = sync.get(String.valueOf(id));
+ try {
+ mapper.readValue(value, CityCountry.class);
+ } catch (JsonProcessingException e) {
+ LOGGER.error("Couldn't test redis data : ", e);
+ e.printStackTrace(System.out);
+ }
+ }
+ }
+ }
+
+ public void testMysqlData(List ids) {
+ try (Session session = sessionFactory.getCurrentSession()) {
+ session.beginTransaction();
+ for (Integer id : ids) {
+ City city = cityService.getById(id);
+ Set languages = city.getCountryId().getLanguages();
+ }
+ session.getTransaction().commit();
+ }
+ }
+
+ public List fetchData() {
+ try (Session session = sessionFactory.getCurrentSession()) {
+ List allCities = new ArrayList<>();
+ session.beginTransaction();
+
+ countryService.getAll();
+
+ int totalCount = cityService.getTotalCount();
+ int step = 500;
+ for (int i = 0; i < totalCount; i += step) {
+ allCities.addAll(cityService.getItems(i, step));
+ }
+ session.getTransaction().commit();
+ return allCities;
+ }
+ }
+}
diff --git a/src/main/java/com/javarush/DataTransformer.java b/src/main/java/com/javarush/DataTransformer.java
new file mode 100644
index 0000000..9803fe7
--- /dev/null
+++ b/src/main/java/com/javarush/DataTransformer.java
@@ -0,0 +1,72 @@
+package com.javarush;
+
+import com.javarush.domain.entity.City;
+import com.javarush.domain.entity.Country;
+import com.javarush.domain.entity.CountryLanguage;
+import com.javarush.redis.CityCountry;
+import com.javarush.redis.Language;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class DataTransformer {
+ public static CityCountry countryTransformToCityCountry(Country country) {
+ CityCountry res = new CityCountry();
+ res.setAlternativeCountryCode(country.getCode2());
+ res.setContinent(country.getContinent());
+ res.setCountryCode(country.getCode());
+ res.setCountryName(country.getName());
+ res.setCountryPopulation(country.getPopulation());
+ res.setCountryRegion(country.getRegion());
+ res.setCountrySurfaceArea(country.getSurfaceArea());
+ Set countryLanguages = country.getLanguages();
+ Set languages = countryLanguages.stream().map(cl -> {
+ Language language = new Language();
+ language.setLanguage(cl.getLanguage());
+ language.setIsOfficial(cl.getIsOfficial());
+ language.setPercentage(cl.getPercentage());
+ return language;
+ }).collect(Collectors.toSet());
+ res.setLanguages(languages);
+ return res;
+ }
+
+ public static CityCountry cityTransformToCityCountry(City city) {
+ CityCountry res = new CityCountry();
+ res.setId(city.getId());
+ res.setName(city.getName());
+ res.setPopulation(city.getPopulation());
+ res.setDistrict(city.getDistrict());
+ return res;
+ }
+
+ public static City cityCountryTransformToCity(CityCountry cityCountry) {
+ City city = new City();
+ city.setId(cityCountry.getId());
+ city.setName(cityCountry.getName());
+ city.setPopulation(cityCountry.getPopulation());
+ city.setDistrict(cityCountry.getDistrict());
+ return city;
+ }
+
+ public static Country cityCountryToCountry(CityCountry cityCountry) {
+ Country country = new Country();
+ country.setCode2(cityCountry.getAlternativeCountryCode());
+ country.setContinent(cityCountry.getContinent());
+ country.setCode(cityCountry.getCountryCode());
+ country.setName(cityCountry.getCountryName());
+ country.setPopulation(cityCountry.getCountryPopulation());
+ country.setRegion(cityCountry.getCountryRegion());
+ country.setSurfaceArea(cityCountry.getCountrySurfaceArea());
+ Set languages = cityCountry.getLanguages();
+ Set countryLanguages = languages.stream().map(cl -> {
+ CountryLanguage countryLanguage = new CountryLanguage();
+ countryLanguage.setLanguage(cl.getLanguage());
+ countryLanguage.setIsOfficial(cl.getIsOfficial());
+ countryLanguage.setPercentage(cl.getPercentage());
+ return countryLanguage;
+ }).collect(Collectors.toSet());
+ country.setLanguages(countryLanguages);
+ return country;
+ }
+}
diff --git a/src/main/java/com/javarush/cache/RedisRepository.java b/src/main/java/com/javarush/cache/RedisRepository.java
new file mode 100644
index 0000000..4803997
--- /dev/null
+++ b/src/main/java/com/javarush/cache/RedisRepository.java
@@ -0,0 +1,58 @@
+package com.javarush.cache;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.javarush.services.CityService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import redis.clients.jedis.UnifiedJedis;
+import redis.clients.jedis.util.SafeEncoder;
+
+public class RedisRepository {
+ private final UnifiedJedis jedis;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private static final String TOPK_NAME = "city-country";
+ private static final int THRESHOLD = 7;
+ private static final Logger LOGGER = LoggerFactory.getLogger(CityService.class);
+ private static final String REDIS_URL = "redis://127.0.0.1:6379";
+ private static final String BRACES_REGEX = "[\\[\\]]";
+
+ public RedisRepository() {
+ jedis = new UnifiedJedis(REDIS_URL);
+
+ jedis.del(TOPK_NAME);
+ if (!jedis.exists(TOPK_NAME)) {
+ jedis.topkReserve(TOPK_NAME, 5L, 2000L, 7L, 0.925D);
+ }
+ }
+
+ public T getById(String key, Class clazz) throws JsonProcessingException {
+ String serializedEntity = jedis.get(key);
+ return deserialize(serializedEntity, clazz);
+ }
+
+ public void put(String key, T value) throws JsonProcessingException {
+ jedis.topkAdd(TOPK_NAME, key);
+
+ if (getCount(key) >= THRESHOLD) {
+ jedis.set(key, serialize(value));
+ }
+ }
+
+ private int getCount(String key) {
+ String count = jedis.sendCommand(() -> SafeEncoder.encode("TOPK.COUNT"), SafeEncoder.encodeMany(TOPK_NAME, key)).toString();
+ return Integer.parseInt(count.replaceAll(BRACES_REGEX, ""));
+ }
+
+ public boolean checkExists(String key) {
+ return jedis.exists(key);
+ }
+
+ private String serialize(T entity) throws JsonProcessingException {
+ return objectMapper.writeValueAsString(entity);
+ }
+
+ private T deserialize(String serializedEntity, Class clazz) throws JsonProcessingException {
+ return objectMapper.readValue(serializedEntity, clazz);
+ }
+}
diff --git a/src/main/java/com/javarush/config/HibernateUtil.java b/src/main/java/com/javarush/config/HibernateUtil.java
new file mode 100644
index 0000000..ed23ee1
--- /dev/null
+++ b/src/main/java/com/javarush/config/HibernateUtil.java
@@ -0,0 +1,46 @@
+package com.javarush.config;
+
+import com.javarush.domain.entity.City;
+import com.javarush.domain.entity.Country;
+import com.javarush.domain.entity.CountryLanguage;
+import org.hibernate.SessionFactory;
+import org.hibernate.cfg.Configuration;
+import org.hibernate.cfg.Environment;
+
+import java.util.Properties;
+
+public class HibernateUtil {
+ private static SessionFactory sessionFactory;
+
+ private HibernateUtil() {
+
+ }
+
+ private static SessionFactory prepareRelationalDb() {
+ final SessionFactory sessionFactory;
+ Properties properties = new Properties();
+ properties.put(Environment.DIALECT, "org.hibernate.dialect.MySQL8Dialect");
+ properties.put(Environment.DRIVER, "com.p6spy.engine.spy.P6SpyDriver");
+ properties.put(Environment.URL, "jdbc:p6spy:mysql://localhost:3306/world");
+ properties.put(Environment.USER, "root");
+ properties.put(Environment.PASS, "root");
+ properties.put(Environment.CURRENT_SESSION_CONTEXT_CLASS, "thread");
+ properties.put(Environment.HBM2DDL_AUTO, "validate");
+ properties.put(Environment.STATEMENT_BATCH_SIZE, "100");
+
+ sessionFactory = new Configuration()
+ .addAnnotatedClass(City.class)
+ .addAnnotatedClass(Country.class)
+ .addAnnotatedClass(CountryLanguage.class)
+ .addProperties(properties)
+ .buildSessionFactory();
+ return sessionFactory;
+ }
+
+ public static SessionFactory getSessionFactory() {
+ if (sessionFactory == null) {
+ sessionFactory = prepareRelationalDb();
+ }
+ return sessionFactory;
+ }
+}
diff --git a/src/main/java/com/javarush/config/RedisConfig.java b/src/main/java/com/javarush/config/RedisConfig.java
new file mode 100644
index 0000000..6e4468e
--- /dev/null
+++ b/src/main/java/com/javarush/config/RedisConfig.java
@@ -0,0 +1,15 @@
+package com.javarush.config;
+
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.RedisURI;
+import io.lettuce.core.api.StatefulRedisConnection;
+
+public class RedisConfig {
+ public static RedisClient prepareRedisClient() {
+ RedisClient redisClient = RedisClient.create(RedisURI.create("localhost", 6379));
+ try (StatefulRedisConnection connection = redisClient.connect()) {
+ System.out.println("\nConnected to Redis\n");
+ }
+ return redisClient;
+ }
+}
diff --git a/src/main/java/com/javarush/domain/entity/City.java b/src/main/java/com/javarush/domain/entity/City.java
new file mode 100644
index 0000000..0a7f00f
--- /dev/null
+++ b/src/main/java/com/javarush/domain/entity/City.java
@@ -0,0 +1,26 @@
+package com.javarush.domain.entity;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@Entity
+@Table(name = "city")
+public class City {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Integer id;
+ @ManyToOne
+ @JoinColumn(name = "country_id")
+ private Country countryId;
+ @Column(name = "name")
+ private String name;
+ @Column(name = "district")
+ private String district;
+ @Column(name = "population")
+ private Integer population;
+}
diff --git a/src/main/java/com/javarush/domain/Country.java b/src/main/java/com/javarush/domain/entity/Country.java
similarity index 82%
rename from src/main/java/com/javarush/domain/Country.java
rename to src/main/java/com/javarush/domain/entity/Country.java
index c2194c3..9f5eedc 100644
--- a/src/main/java/com/javarush/domain/Country.java
+++ b/src/main/java/com/javarush/domain/entity/Country.java
@@ -1,18 +1,23 @@
-package com.javarush.domain;
+package com.javarush.domain.entity;
+import com.javarush.domain.enums.Continent;
import jakarta.persistence.*;
-import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
import java.math.BigDecimal;
import java.util.Set;
-@Data
+@Getter
+@Setter
+@NoArgsConstructor
@Entity
@Table(name = "country")
public class Country {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
- private int id;
+ private Integer id;
@Column(name = "code")
private String code;
@Column(name = "code_2")
@@ -27,9 +32,9 @@ public class Country {
@Column(name = "surface_area")
private BigDecimal surfaceArea;
@Column(name = "indep_year")
- private short indepYear;
+ private Short indepYear;
@Column(name = "population")
- private int population;
+ private Integer population;
@Column(name = "life_expectancy")
private BigDecimal lifeExpectancy;
@Column(name = "gnp")
diff --git a/src/main/java/com/javarush/domain/entity/CountryLanguage.java b/src/main/java/com/javarush/domain/entity/CountryLanguage.java
new file mode 100644
index 0000000..9fc0514
--- /dev/null
+++ b/src/main/java/com/javarush/domain/entity/CountryLanguage.java
@@ -0,0 +1,31 @@
+package com.javarush.domain.entity;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.hibernate.annotations.Type;
+
+import java.math.BigDecimal;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@Entity
+@Table(name = "country_language")
+public class CountryLanguage {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Integer id;
+ @ManyToOne
+ @JoinColumn(name = "country_id")
+ private Country countryId;
+ @Column(name = "language")
+ private String language;
+ @Column(name = "is_official", columnDefinition = "BIT")
+ @Type(type = "org.hibernate.type.NumericBooleanType")
+ private Boolean isOfficial;
+ @Column(name = "percentage")
+ private BigDecimal percentage;
+
+}
diff --git a/src/main/java/com/javarush/domain/enums/Continent.java b/src/main/java/com/javarush/domain/enums/Continent.java
new file mode 100644
index 0000000..7d8b583
--- /dev/null
+++ b/src/main/java/com/javarush/domain/enums/Continent.java
@@ -0,0 +1,11 @@
+package com.javarush.domain.enums;
+
+public enum Continent {
+ ASIA,
+ EUROPE,
+ NORTH_AMERICA,
+ AFRICA,
+ OCEANIA,
+ ANTARCTICA,
+ SOUTH_AMERICA
+}
diff --git a/src/main/java/com/javarush/domain/exceptions/CityException.java b/src/main/java/com/javarush/domain/exceptions/CityException.java
new file mode 100644
index 0000000..b5efd0b
--- /dev/null
+++ b/src/main/java/com/javarush/domain/exceptions/CityException.java
@@ -0,0 +1,7 @@
+package com.javarush.domain.exceptions;
+
+public class CityException extends RuntimeException {
+ public CityException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/javarush/domain/exceptions/CountryException.java b/src/main/java/com/javarush/domain/exceptions/CountryException.java
new file mode 100644
index 0000000..6f278c1
--- /dev/null
+++ b/src/main/java/com/javarush/domain/exceptions/CountryException.java
@@ -0,0 +1,7 @@
+package com.javarush.domain.exceptions;
+
+public class CountryException extends RuntimeException {
+ public CountryException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/javarush/domain/exceptions/RedisException.java b/src/main/java/com/javarush/domain/exceptions/RedisException.java
new file mode 100644
index 0000000..41ace4d
--- /dev/null
+++ b/src/main/java/com/javarush/domain/exceptions/RedisException.java
@@ -0,0 +1,7 @@
+package com.javarush.domain.exceptions;
+
+public class RedisException extends RuntimeException {
+ public RedisException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/javarush/redis/CityCountry.java b/src/main/java/com/javarush/redis/CityCountry.java
new file mode 100644
index 0000000..5f64121
--- /dev/null
+++ b/src/main/java/com/javarush/redis/CityCountry.java
@@ -0,0 +1,34 @@
+package com.javarush.redis;
+
+import com.javarush.domain.enums.Continent;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Set;
+
+@Data
+public class CityCountry {
+ private Integer id;
+
+ private String name;
+
+ private String district;
+
+ private Integer population;
+
+ private String countryCode;
+
+ private String alternativeCountryCode;
+
+ private String countryName;
+
+ private Continent continent;
+
+ private String countryRegion;
+
+ private BigDecimal countrySurfaceArea;
+
+ private Integer countryPopulation;
+
+ private Set languages;
+}
diff --git a/src/main/java/com/javarush/redis/Language.java b/src/main/java/com/javarush/redis/Language.java
new file mode 100644
index 0000000..c91d80a
--- /dev/null
+++ b/src/main/java/com/javarush/redis/Language.java
@@ -0,0 +1,12 @@
+package com.javarush.redis;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class Language {
+ private String language;
+ private Boolean isOfficial;
+ private BigDecimal percentage;
+}
diff --git a/src/main/java/com/javarush/repository/CityRepository.java b/src/main/java/com/javarush/repository/CityRepository.java
new file mode 100644
index 0000000..0420776
--- /dev/null
+++ b/src/main/java/com/javarush/repository/CityRepository.java
@@ -0,0 +1,67 @@
+package com.javarush.repository;
+
+import com.javarush.config.HibernateUtil;
+import com.javarush.domain.entity.City;
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+import org.hibernate.Transaction;
+
+import java.util.List;
+
+public class CityRepository implements CrudRepository {
+ private final SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
+
+ @Override
+ public City getById(Integer id) {
+ try (Session session = sessionFactory.openSession()) {
+ return session.createQuery("select c from City c join fetch c.countryId where c.id = :ID", City.class)
+ .setParameter("ID", id)
+ .uniqueResult();
+ }
+ }
+
+ @Override
+ public City save(City entity) {
+ try (Session session = sessionFactory.getCurrentSession()) {
+ Transaction transaction = session.beginTransaction();
+ session.persist(entity);
+ transaction.commit();
+ return entity;
+ }
+ }
+
+ @Override
+ public void delete(Integer id) {
+ try (Session session = sessionFactory.getCurrentSession()) {
+ Transaction transaction = session.beginTransaction();
+ session.createQuery("delete from City c where c.id = :ID", City.class)
+ .setParameter("ID", id)
+ .executeUpdate();
+ transaction.commit();
+ }
+ }
+
+ @Override
+ public List getAll() {
+ try (Session session = sessionFactory.openSession()) {
+ return session.createQuery("select c from City c", City.class).list();
+ }
+ }
+
+ @Override
+ public List getItems(int offset, int limit) {
+ try(Session session = sessionFactory.openSession()) {
+ return session.createQuery("select c from City c", City.class)
+ .setFirstResult(offset)
+ .setMaxResults(limit)
+ .list();
+ }
+ }
+
+ @Override
+ public int getTotalCount() {
+ try(Session session = sessionFactory.openSession()) {
+ return Math.toIntExact(session.createQuery("select count(c) from City c", Long.class).uniqueResult());
+ }
+ }
+}
diff --git a/src/main/java/com/javarush/repository/CountryRepository.java b/src/main/java/com/javarush/repository/CountryRepository.java
new file mode 100644
index 0000000..7207b59
--- /dev/null
+++ b/src/main/java/com/javarush/repository/CountryRepository.java
@@ -0,0 +1,66 @@
+package com.javarush.repository;
+
+import com.javarush.config.HibernateUtil;
+import com.javarush.domain.entity.Country;
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+
+import java.util.List;
+
+public class CountryRepository implements CrudRepository {
+ private final SessionFactory sessionFactory = HibernateUtil.getSessionFactory();
+
+ @Override
+ public Country getById(Integer id) {
+ try (Session session = sessionFactory.openSession()) {
+ return session.createQuery("select c from Country c where c.id = :ID", Country.class)
+ .setParameter("ID", id)
+ .getSingleResult();
+ }
+ }
+
+ @Override
+ public Country save(Country entity) {
+ try (Session session = sessionFactory.getCurrentSession()) {
+ session.beginTransaction();
+ session.persist(entity);
+ session.getTransaction().commit();
+ return entity;
+ }
+ }
+
+ @Override
+ public void delete(Integer id) {
+ try (Session session = sessionFactory.getCurrentSession()) {
+ session.beginTransaction();
+ session.createQuery("delete from Country c where c.id = :ID", Country.class)
+ .setParameter("ID", id)
+ .executeUpdate();
+ session.getTransaction().commit();
+ }
+ }
+
+ @Override
+ public List getAll() {
+ try (Session session = sessionFactory.openSession()) {
+ return session.createQuery("select c from Country c join fetch c.languages", Country.class).list();
+ }
+ }
+
+ @Override
+ public List getItems(int offset, int limit) {
+ try (Session session = sessionFactory.getCurrentSession()) {
+ return session.createQuery("select c from Country c", Country.class)
+ .setFirstResult(offset)
+ .setMaxResults(limit)
+ .list();
+ }
+ }
+
+ @Override
+ public int getTotalCount() {
+ try (Session session = sessionFactory.getCurrentSession()) {
+ return Math.toIntExact(session.createQuery("select count(c) from Country c", Long.class).uniqueResult());
+ }
+ }
+}
diff --git a/src/main/java/com/javarush/repository/CrudRepository.java b/src/main/java/com/javarush/repository/CrudRepository.java
new file mode 100644
index 0000000..4461b32
--- /dev/null
+++ b/src/main/java/com/javarush/repository/CrudRepository.java
@@ -0,0 +1,17 @@
+package com.javarush.repository;
+
+import java.util.List;
+
+public interface CrudRepository {
+ T getById(I id);
+
+ T save(T entity);
+
+ void delete(I id);
+
+ List getAll();
+
+ List getItems(int offset, int limit);
+
+ int getTotalCount();
+}
diff --git a/src/main/java/com/javarush/services/CityService.java b/src/main/java/com/javarush/services/CityService.java
new file mode 100644
index 0000000..86d90fa
--- /dev/null
+++ b/src/main/java/com/javarush/services/CityService.java
@@ -0,0 +1,101 @@
+package com.javarush.services;
+
+import com.javarush.DataTransformer;
+import com.javarush.cache.RedisRepository;
+import com.javarush.domain.entity.City;
+import com.javarush.domain.exceptions.CityException;
+import com.javarush.redis.CityCountry;
+import com.javarush.repository.CityRepository;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+public class CityService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(CityService.class);
+ private final RedisRepository redisRepository;
+ private final CityRepository cityRepository;
+
+ public City getById(Integer id) {
+ String key = "city_" + id;
+ try {
+ if(redisRepository.checkExists(key)){
+ CityCountry cityCountry = redisRepository.getById(key, CityCountry.class);
+ return DataTransformer.cityCountryTransformToCity(cityCountry);
+ }
+ City city = cityRepository.getById(id);
+ redisRepository.put(key, DataTransformer.cityTransformToCityCountry(city));
+ return city;
+ } catch (Exception e) {
+ LOGGER.error("Cannot get city with id : {} ", id, e);
+ throw new CityException("ERROR :: cannot get city with id " + id + " " + e.getMessage());
+ }
+ }
+
+ public City save(City entity) {
+ try {
+ City city = new CityRepository().save(entity);
+ if (city == null) {
+ LOGGER.error("No city with id {}", entity.getId());
+ throw new CityException("ERROR :: no city with id " + entity.getId());
+ }
+ return city;
+ } catch (Exception e) {
+ LOGGER.error("Cannot save city with id {}", entity.getId(), e);
+ throw new CityException("ERROR :: cannot save city with id " + entity.getId() + " " + e.getMessage());
+ }
+ }
+
+ public void delete(Integer id) {
+ try {
+ new CityRepository().delete(id);
+ } catch (Exception e) {
+ LOGGER.error("Cannot delete city with id: {}", id, e);
+ throw new CityException("ERROR :: cannot delete city with id: " + id + " " + e.getMessage());
+ }
+ }
+
+ public List getAll() {
+ try {
+ List cities = new CityRepository().getAll();
+ if (cities.isEmpty()) {
+ LOGGER.error("No cities found");
+ throw new CityException("ERROR :: no cities found");
+ }
+ return cities;
+ } catch (Exception e) {
+ LOGGER.error("Cannot get all cities : ", e);
+ throw new CityException("ERROR :: cannot get all cities: " + e.getMessage());
+ }
+ }
+
+ public List getItems(int offset, int limit) {
+ try {
+ List cities = new CityRepository().getItems(offset, limit);
+ if (cities.isEmpty()) {
+ LOGGER.error("No cities found in range");
+ throw new CityException("ERROR :: no cities found in range");
+ }
+ return cities;
+ } catch (Exception e) {
+ LOGGER.error("Cannot get all cities in range : ", e);
+ throw new CityException("ERROR :: cannot get all cities in range: " + e.getMessage());
+ }
+ }
+
+ public int getTotalCount() {
+ try {
+ int totalCount = new CityRepository().getTotalCount();
+ if (totalCount == 0) {
+ LOGGER.error("Total cities count is 0");
+ throw new CityException("ERROR :: total cities count is 0");
+ }
+ return totalCount;
+ } catch (Exception e) {
+ LOGGER.error("Cannot get all cities count : ", e);
+ throw new CityException("ERROR :: cannot get all cities count: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/javarush/services/CountryService.java b/src/main/java/com/javarush/services/CountryService.java
new file mode 100644
index 0000000..ba244b0
--- /dev/null
+++ b/src/main/java/com/javarush/services/CountryService.java
@@ -0,0 +1,101 @@
+package com.javarush.services;
+
+import com.javarush.DataTransformer;
+import com.javarush.cache.RedisRepository;
+import com.javarush.domain.entity.Country;
+import com.javarush.domain.exceptions.CountryException;
+import com.javarush.redis.CityCountry;
+import com.javarush.repository.CountryRepository;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+public class CountryService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(CountryService.class);
+ private final RedisRepository redisRepository;
+ private final CountryRepository countryRepository;
+
+ public Country getById(Integer id) {
+ String key = "country_" + id;
+ try {
+ if (redisRepository.checkExists(key)) {
+ CityCountry cityCountry = redisRepository.getById(key, CityCountry.class);
+ return DataTransformer.cityCountryToCountry(cityCountry);
+ }
+ Country country = countryRepository.getById(id);
+ redisRepository.put(key, DataTransformer.countryTransformToCityCountry(country));
+ return country;
+ } catch (Exception e) {
+ LOGGER.error("Cannot get country with id {}", id, e);
+ throw new CountryException("ERROR :: cannot get country with id " + id + " " + e.getMessage());
+ }
+ }
+
+ public Country save(Country entity) {
+ try {
+ Country savedCountry = countryRepository.save(entity);
+ if (savedCountry == null) {
+ LOGGER.error("Failed to save country with id {}", entity.getId());
+ throw new CountryException("ERROR :: no country with id " + entity.getId());
+ }
+ return savedCountry;
+ } catch (Exception ex) {
+ LOGGER.error("Could not save country with id {}", entity.getId(), ex);
+ throw new CountryException("ERROR :: could not save country with id " + entity.getId() + " " + ex.getMessage());
+ }
+ }
+
+ public void delete(Integer id) {
+ try {
+ countryRepository.delete(id);
+ } catch (Exception e) {
+ LOGGER.error("Cannot delete country with id: {}", id, e);
+ throw new CountryException("ERROR :: cannot delete country with id: " + id + " " + e.getMessage());
+ }
+ }
+
+ public List getAll() {
+ try {
+ List countries = countryRepository.getAll();
+ if (countries.isEmpty()) {
+ LOGGER.error("No countries found");
+ throw new CountryException("ERROR :: no countries found");
+ }
+ return countries;
+ } catch (Exception e) {
+ LOGGER.error("Cannot get all countries : ", e);
+ throw new CountryException("ERROR :: cannot get all countries: " + e.getMessage());
+ }
+ }
+
+ public List getItems(int offset, int limit) {
+ try {
+ List countries = countryRepository.getItems(offset, limit);
+ if (countries.isEmpty()) {
+ LOGGER.error("No countries found in range");
+ throw new CountryException("ERROR :: no countries found in range");
+ }
+ return countries;
+ } catch (Exception e) {
+ LOGGER.error("Cannot get all countries in range : ", e);
+ throw new CountryException("ERROR :: cannot get all countries in range: " + e.getMessage());
+ }
+ }
+
+ public int getTotalCount() {
+ try {
+ int totalCount = countryRepository.getTotalCount();
+ if (totalCount == 0) {
+ LOGGER.error("Total countries count is 0");
+ throw new CountryException("ERROR :: total countries count is 0");
+ }
+ return totalCount;
+ } catch (Exception e) {
+ LOGGER.error("Cannot get all countries count : ", e);
+ throw new CountryException("ERROR :: cannot get all countries count: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/com/javarush/services/CityServiceTest.java b/src/test/java/com/javarush/services/CityServiceTest.java
new file mode 100644
index 0000000..f90b4a8
--- /dev/null
+++ b/src/test/java/com/javarush/services/CityServiceTest.java
@@ -0,0 +1,116 @@
+package com.javarush.services;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.javarush.DataTransformer;
+import com.javarush.cache.RedisRepository;
+import com.javarush.domain.entity.City;
+import com.javarush.domain.exceptions.CityException;
+import com.javarush.redis.CityCountry;
+import com.javarush.repository.CityRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class CityServiceTest {
+
+ @Mock
+ private RedisRepository redisRepository;
+
+ @Mock
+ private CityRepository cityRepository;
+
+ @InjectMocks
+ private CityService cityService;
+
+ private City city;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.initMocks(this);
+ city = new City();
+ }
+
+ @Test
+ void testGetById_CityExistsInRedis() throws JsonProcessingException {
+ int id = 1;
+ String key = "city_" + id;
+ CityCountry testCityCountry = DataTransformer.cityTransformToCityCountry(city);
+
+ when(redisRepository.checkExists(key)).thenReturn(true);
+ when(redisRepository.getById(key, CityCountry.class)).thenReturn(testCityCountry);
+
+ City city = cityService.getById(id);
+
+ assertNotNull(city);
+ assertEquals(city.getId(), city.getId());
+ verify(redisRepository, times(1)).getById(key, CityCountry.class);
+ }
+
+ @Test
+ void testGetById_CityNotInRedis() throws Exception {
+ int cityId = 1;
+ String key = "city_" + cityId;
+
+ when(redisRepository.checkExists(key)).thenReturn(false);
+ when(cityRepository.getById(cityId)).thenReturn(city);
+
+ City result = cityService.getById(cityId);
+
+ assertNotNull(result);
+ verify(redisRepository, times(1)).put(eq(key), any(CityCountry.class));
+ assertEquals(city, result);
+ }
+
+ @Test
+ void testGetById_ExceptionThrown() {
+ int cityId = 1;
+ String key = "city_" + cityId;
+
+ when(redisRepository.checkExists(key)).thenThrow(new RuntimeException("Test exception"));
+
+ Exception exception = assertThrows(CityException.class, () -> cityService.getById(cityId));
+ assertTrue(exception.getMessage().contains("ERROR :: cannot get city with id"));
+ }
+
+ @Test
+ void testSave_Successful() throws Exception {
+ when(cityRepository.save(city)).thenReturn(city);
+
+ City result = cityService.save(city);
+
+ assertNotNull(result);
+ assertEquals(city, result);
+ }
+
+ @Test
+ void testSave_ExceptionThrown() {
+ when(cityRepository.save(city)).thenThrow(new RuntimeException("Test exception"));
+
+ Exception exception = assertThrows(CityException.class, () -> cityService.save(city));
+ assertTrue(exception.getMessage().contains("ERROR :: cannot save city with id"));
+ }
+
+ @Test
+ void testDelete_Successful() throws Exception {
+ int cityId = 1;
+
+ doNothing().when(cityRepository).delete(cityId);
+
+ assertDoesNotThrow(() -> cityService.delete(cityId));
+ }
+
+ @Test
+ void testDelete_ExceptionThrown() {
+ int cityId = 1;
+
+ doThrow(new RuntimeException("Test exception")).when(cityRepository).delete(cityId);
+
+ Exception exception = assertThrows(CityException.class, () -> cityService.delete(cityId));
+ assertTrue(exception.getMessage().contains("ERROR :: cannot delete city with id"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/javarush/services/CountryServiceTest.java b/src/test/java/com/javarush/services/CountryServiceTest.java
new file mode 100644
index 0000000..2d34758
--- /dev/null
+++ b/src/test/java/com/javarush/services/CountryServiceTest.java
@@ -0,0 +1,117 @@
+package com.javarush.services;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.javarush.DataTransformer;
+import com.javarush.cache.RedisRepository;
+import com.javarush.domain.entity.Country;
+import com.javarush.domain.exceptions.CountryException;
+import com.javarush.redis.CityCountry;
+import com.javarush.repository.CountryRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+class CountryServiceTest {
+ @Mock
+ private RedisRepository redisRepository;
+
+ @Mock
+ private CountryRepository countryRepository;
+
+ @InjectMocks
+ private CountryService countryService;
+
+ private Country country;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.initMocks(this);
+ country = new Country();
+ }
+
+ @Test
+ void testGetById_CountryExistsInRedis() throws JsonProcessingException {
+ int id = 1;
+ String key = "country_" + id;
+ CityCountry testCityCountry = DataTransformer.countryTransformToCityCountry(country);
+
+ when(redisRepository.checkExists(key)).thenReturn(true);
+ when(redisRepository.getById(key, CityCountry.class)).thenReturn(testCityCountry);
+
+ Country country1 = countryService.getById(id);
+
+ assertNotNull(country1);
+ assertEquals(country1, country);
+ verify(redisRepository, times(1)).getById(key, CityCountry.class);
+ }
+
+ @Test
+ void testGetById_CountryNotInRedis() throws Exception {
+ int countryId = 1;
+ String key = "country_" + countryId;
+
+ when(redisRepository.checkExists(key)).thenReturn(false);
+ when(countryRepository.getById(countryId)).thenReturn(country);
+
+ Country result = countryService.getById(countryId);
+
+ assertNotNull(result);
+ verify(redisRepository, times(1)).put(eq(key), any(CityCountry.class));
+ assertEquals(country, result);
+ }
+
+ @Test
+ void testGetById_ExceptionThrown() {
+ int countryId = 1;
+ String key = "country_" + countryId;
+
+ when(redisRepository.checkExists(key)).thenThrow(new RuntimeException("Test exception"));
+
+ Exception exception = assertThrows(CountryException.class, () -> countryService.getById(countryId));
+ assertTrue(exception.getMessage().contains("ERROR :: cannot get country with id"));
+ }
+
+ @Test
+ void testSave_Successful() throws Exception {
+ when(countryRepository.save(country)).thenReturn(country);
+
+ Country result = countryService.save(country);
+
+ assertNotNull(result);
+ assertEquals(country, result);
+ }
+
+ @Test
+ void testSave_ExceptionThrown() {
+ when(countryRepository.save(country)).thenThrow(new RuntimeException("Test exception"));
+
+ Exception exception = assertThrows(CountryException.class, () -> countryService.save(country));
+ assertTrue(exception.getMessage().contains("ERROR :: could not save country with id"));
+ }
+
+ @Test
+ void testDelete_Successful() throws Exception {
+ int countryId = 1;
+
+ doNothing().when(countryRepository).delete(countryId);
+
+ assertDoesNotThrow(() -> countryService.delete(countryId));
+ }
+
+ @Test
+ void testDelete_ExceptionThrown() {
+ int countryId = 1;
+
+ doThrow(new RuntimeException("Test exception")).when(countryRepository).delete(countryId);
+
+ Exception exception = assertThrows(CountryException.class, () -> countryService.delete(countryId));
+ assertTrue(exception.getMessage().contains("ERROR :: cannot delete country with id"));
+ }
+}
\ No newline at end of file