JavaRush /Курсы /Hibernate deep-dive /Refactoring patterns: spli...

Refactoring patterns: split, queries, UoW

Hibernate deep-dive
29 уровень, 3 лекция
Открыта

1. Refactoring patterns: маленькие шаги

Если вы хоть раз пробовали «навести порядок в persistence layer», вы знаете классический сценарий: открываешь сервис, видишь 300 строк, 12 вызовов репозитория и где‑то между ними saveAndFlush(), а потом думаешь: “Ну… давайте для начала поменяем LAZY на EAGER, вдруг поможет”. Это нормальная человеческая реакция, но она редко приводит к хорошему коду. Нам нужны refactoring patterns именно для того, чтобы мозг перестал метаться.

Refactoring pattern в контексте Hibernate — это маленькое, повторяемое улучшение, которое делает поведение ORM более предсказуемым. Не «ускорит всё в 10 раз», не «уберёт все баги навсегда», а именно: уменьшит область неопределённости. Hibernate и так «умный» (иногда даже слишком), поэтому наша главная задача — сделать так, чтобы он был умным в понятных границах.

Три паттерна здесь полезны именно тем, что хорошо лечат три типовые болезни.

Read-model split лечит entity leakage: вы перестаёте отдавать entity наружу, когда вам надо просто показать список или сделать экспорт. Entity остаётся write‑моделью, а чтение получает свою форму данных.

Явный запрос (explicit query) лечит «магический репозиторий»: вместо надежды, что findById() как‑нибудь сам «правильно всё загрузит», вы прямо фиксируете форму чтения и fetch‑план под use case.

Более узкий unit of work лечит overscoped transactions: транзакция перестаёт включать в себя «всё подряд», а Hibernate перестаёт жить слишком долго и делать SQL в неожиданные моменты.

Чтобы это запомнилось не как три лозунга, а как рабочий инструмент, давайте зафиксируем простую табличку — не как чек‑лист «делай так», а как карту «что лечим чем».

Симптом в коде Что обычно чувствуется в проде Какой refactoring move чаще всего помогает Типичный пример в Commerce Persistence Lab
PurchaseOrder непредсказуемые lazy‑подзагрузки, LazyInitializationException или “почему столько SQL?” read-model split список заказов / summary заказа
findById() и дальше “ходит по графу” N+1, вторичные select’ы, странная цена “просто чтения” findForEditingById(), findSummaryById()
Один метод транзакционный и делает всё: читает, меняет, форматирует, логирует долгие транзакции, лишний flush, блокировки, тяжёлая диагностика smaller unit of work закрытие заказов + генерация отчёта

Сначала полезно посмотреть на каждый move отдельно, а потом — как они собираются в один сценарий «до/после», чтобы было видно: это не теория, а вполне конкретные куски кода.

2. Read-model split: чтение vs запись

Когда начинаешь писать Spring Data JPA проект, очень хочется сделать так: «всё будет entity, а DTO — это потом». В какой‑то момент это даже работает. Но ближе к реальности возникает неприятное свойство: entity — штука “живая”. Она может быть managed, может быть proxy, может лениво подгружать связи и может участвовать в dirty checking. Если вы отдаёте entity туда, где она не должна жить, вы буквально выпускаете кота на улицу: вернётся — не факт, но сюрпризы принесёт точно.

Суть read-model split простая: entity — это write‑модель, то есть форма данных, удобная для изменения внутри транзакции. А read use case (список, карточка summary, экспорт, отчёт) должен получать свою форму, обычно в виде DTO/projection. Это не «DDD религия», а практическая санитария: так вы не тащите managed‑граф туда, где нужен просто набор колонок.

В нашем Commerce Persistence Lab типичный пример — список товаров для админки. Для списка нам обычно нужны id, sku, name, возможно status и цена. А ProductDetails, assignments по категориям, аудит и прочее — это для других use cases. Поэтому мы делаем узкую read‑модель.

Пример: простой DTO для списка товаров

Начнём с record (в Java 25 это очень удобная форма “данные без лишней философии”):

package com.example.commerce.catalog.dto;

// DTO для списка: только то, что нужно для табличного вывода, без entity-графа и ленивых связей.
public record ProductListRow(Long id, String sku, String name) {
}

Обратите внимание: это не “универсальный DTO на все случаи жизни”. Он специально скучный и короткий. Если DTO начинает разрастаться до «всё о товаре, включая мысли автора», он превращается в giant DTO и повторяет судьбу giant entity graph.

Пример: query‑репозиторий, который возвращает DTO

Дальше мы делаем query‑ориентированный репозиторий. Его задача — не «всё CRUD», а ровно один read‑сценарий: получить строки для списка.

package com.example.commerce.catalog.query;

import com.example.commerce.catalog.dto.ProductListRow;
import com.example.commerce.catalog.entity.Product;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;

import java.util.List;

// Репозиторий под чтение: возвращаем DTO и тем самым фиксируем форму данных.
public interface ProductQueryRepository extends Repository<Product, Long> {

    // Запрос сразу собирает "строки" для UI: Hibernate не сможет случайно дотянуть связи по геттерам.
    @Query("""
        select new com.example.commerce.catalog.dto.ProductListRow(p.id, p.sku, p.name)
        from Product p
        where p.status = 'ACTIVE'
        order by p.name
    """)
    List<ProductListRow> findActiveRows();
}

Здесь сразу несколько важных моментов, которые помогают именно Hibernate‑мышлению.

Во‑первых, запрос фиксирует форму данных. Hibernate не может «случайно» загрузить ProductDetails и коллекции, потому что мы не грузим entity — мы строим DTO напрямую.

Во‑вторых, это почти всегда дешевле: меньше колонок в SQL, меньше данных в сети между БД и приложением, меньше объектов в памяти, меньше overhead на persistence context.

В‑третьих, исчезает часть случайных побочных эффектов. DTO не lazy‑прокси, DTO не “managed”. Он тупо данные.

Пример: read‑сервис, который НЕ возвращает entity

Теперь делаем сервис чтения. Здесь ключевой момент — не “обернуть репозиторий сервисом ради архитектурной галочки”, а задать границу: read‑сервис возвращает read‑модель.

package com.example.commerce.catalog.service;

import com.example.commerce.catalog.dto.ProductListRow;
import com.example.commerce.catalog.query.ProductQueryRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class ProductReadService {

    private final ProductQueryRepository productQueryRepository;

    public ProductReadService(ProductQueryRepository productQueryRepository) {
        this.productQueryRepository = productQueryRepository;
    }

    @Transactional(readOnly = true) // Явно помечаем: это чтение, без намерения что-либо изменять и флашить.
    public List<ProductListRow> listActiveProducts() {
        return productQueryRepository.findActiveRows();
    }
}

@Transactional(readOnly = true) здесь не «потому что так красивее». Мы уже обсуждали, что для Hibernate read‑only режим влияет на flush expectations и может снижать лишний dirty checking overhead. Но главное даже не в оптимизации, а в семантике: мы явно говорим «это чтение», и не превращаем метод в скрытый write‑сценарий.

Важный нюанс: projection тоже может раскрыть join шире, чем кажется

Выглядит заманчиво написать что‑то вроде ProductListRow(Long id, String sku, String categoryName) и “просто достать имя категории”. Но как только DTO содержит поля из связанных сущностей, вы начали выбирать fetch‑форму запроса. Это нормально, просто делайте это осознанно.

Если вы добавляете c.name, вам понадобится join, и SQL станет шире. Это не плохо и не хорошо — это решение. Проблема начинается, когда разработчик думает «я же вернул DTO, значит всё безопасно», а по факту сделал запрос с большим join, который тянет больше строк и может раздувать результат. Read-model split не отменяет необходимости думать о форме запроса — он просто делает эту форму более явной.

Мини‑схема: что меняется при read-model split

flowchart TD
    A["Service"] -->|возвращает entity| B["Внешний код"]
    B -->|случайно трогает lazy| C["SQL surprises"]
    A2["Read Service"] -->|возвращает DTO| B2["Внешний код"]
    B2 -->|DTO нельзя дочитать| D["Предсказуемое поведение"]

Если вам нужно запомнить одну мысль из этого раздела, пусть будет такая: entity — это не «формат данных», а «объект под управлением ORM». Поэтому отдавать entity наружу для обычных read‑сценариев — почти всегда подозрительно.

Giant graph: не всякий случай лечится только fetch‑планом

Здесь полезно отделить две похожие боли. Иногда giant graph появляется потому, что read-case пытаются обслужить entity‑графом: списку или summary просто не нужен весь PurchaseOrder, и тогда помогают read-model split и явный query. Но бывает и другая история: root сам по себе оказался слишком широким и тянет за собой коллекции, каскады и lifecycle‑правила, которые не обязаны жить вместе.

Aggregate boundary здесь — это просто граница того, что действительно должно меняться атомарно вместе в одном unit of work. Если заказ почти в каждом write-case тянет полпроекта, проблема уже не только в fetch‑плане. Вы, возможно, случайно сделали один огромный lifecycle‑кусок там, где лучше было бы держать более узкие границы.

Что видно Что это чаще означает Первый ход
giant graph нужен в основном для чтения списка, карточки, экспорта read overfetch, форма данных не совпала с use case read-model split + explicit query / fetch-plan
giant graph всплывает почти в каждом изменении, потому что root тянет несвязанные коллекции и каскады слишком широкий aggregate / lifecycle boundary пересмотреть, что действительно должно жить и меняться в одном unit of work

Эта мысль не отменяет DTO и explicit queries. Она просто не даёт лечить любую проблему одним и тем же fetch‑трюком: иногда нужно сузить чтение, а иногда — честно признать, что write‑модель взяла на себя слишком много.

3. Явные запросы: репозиторий по use case

Есть стадия зрелости проекта, где findById() и findAll() перестают быть удобными и начинают быть опасными. Не потому что они плохие, а потому что они слишком универсальные. Универсальный метод не может знать ваш use case. А Hibernate не умеет читать мысли разработчика (иногда, правда, создаётся ощущение, что он пытается, но получается… как получится).

Явный запрос — это когда repository/query‑метод называется и реализуется под конкретный сценарий, и в этом же месте зафиксированы важные решения: форма выборки, нужные join’ы, нужный fetch‑plan, иногда lock‑режим или сортировка. То есть «я читаю/меняю вот это и вот так», а не «ну я достану entity, а дальше посмотрим».

В Commerce Persistence Lab очень типичный пример — загрузка заказа для редактирования. Если вы делаете orderRepository.findById(id) и дальше в сервисе обращаетесь к order.getItems(), вы опять возвращаетесь к “fetch определяется тем, кто вызвал геттер”. Это дорога к сюрпризам.

Пример: метод репозитория “для редактирования” с явным fetch‑планом

Мы можем сделать метод с @EntityGraph (или JPQL join fetch, если вам так проще читать). Здесь смысл не в том, какой инструмент, а в том, что метод существует под use case.

package com.example.commerce.orders.repository;

import com.example.commerce.orders.entity.PurchaseOrder;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface OrderRepository extends JpaRepository<PurchaseOrder, Long> {

    // Для сценария "редактирование" фиксируем, что нужны customer и items (а не "потом где-то геттер вызовут").
    @EntityGraph(attributePaths = {"customer", "items"})
    Optional<PurchaseOrder> findForEditingById(Long id);
}

Что даёт такой метод?

Он делает границу загрузки данных явной. Если use case “редактирование” требует клиента и позиции, то это видно по коду репозитория, а не спрятано в “где‑то там геттер вызвали”.

Он уменьшает риск N+1. Мы уже знаем, что EAGER не всегда даёт один запрос, а LAZY может стать вторичными select’ами. Здесь вы фиксируете: “в этом сценарии я хочу загрузить вот это”.

Он улучшает code review. Метод findForEditingById() сам по себе заставляет спросить: «а почему для редактирования нужны items?» — и это хороший вопрос. С findById() такой вопрос обычно не возникает, потому что “ну это же просто find”.

Пример: query‑репозиторий для “summary” вместо entity

Иногда вам нужен не целый заказ, а summary: номер и email клиента. Это классический read-model split, но я включу его сюда как пример explicit query: метод явно выражает форму данных и join.

package com.example.commerce.orders.query;

import com.example.commerce.orders.dto.OrderSummary;
import com.example.commerce.orders.entity.PurchaseOrder;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.Param;

public interface OrderQueryRepository extends Repository<PurchaseOrder, Long> {

    // Важно: возвращаем не entity, а DTO-сводку, чтобы чтение было предсказуемым и "узким".
    @Query("""
        select new com.example.commerce.orders.dto.OrderSummary(o.id, o.orderNumber, c.email)
        from PurchaseOrder o
        join o.customer c
        where o.id = :id
    """)
    OrderSummary findSummaryById(@Param("id") Long id);
}

И сам DTO:

package com.example.commerce.orders.dto;

// Сводка по заказу для чтения: минимум данных, чтобы не тащить граф PurchaseOrder наружу.
public record OrderSummary(Long id, String orderNumber, String customerEmail) {
}

Здесь важно то, что метод называется не “findSomething”, а отражает конкретный read‑use case. В зрелом коде это выглядит скучно, но работает как дорожная разметка: вы меньше ошибаетесь, потому что меньше “договариваете” поведение у себя в голове.

«Явный запрос» — это ещё и про намерение

Очень частый анти‑паттерн в persistence layer — когда код выглядит как чтение, но делает запись, или наоборот. Например, метод возвращает List<PurchaseOrder>, но по пути меняет статус или трогает связи, и вы получаете flush в неожиданный момент. Явный запрос помогает отделять чтение от записи, потому что у read‑методов проще держать дисциплину: read‑only транзакция, DTO, фиксированная форма данных.

Небольшая схема: «универсально» vs «явно»

flowchart TD
    S["Service"] --> R1["findById"]
    R1 -->|entity| S
    S -->|где-то позже| L["Lazy access"]
    L --> Q["SQL surprises"]

    S2["Service"] --> R2["findForEditingById"]
    R2 -->|entity with planned fetch| S2
    S2 --> OK["Predictable SQL boundary"]

Если честно, самый большой плюс explicit queries в том, что они снимают с вас необходимость “помнить всё”. Вы перестаёте быть человеком‑кэшем для того, какие связи где нужны. Код сам об этом говорит.

4. Smaller unit of work: сужаем транзакцию

Длинная транзакция в Hibernate‑мире редко является просто “длинной транзакцией”. Обычно это ещё и большой persistence context, больше dirty checking работы, больше шанс получить flush “не в тот момент”, больше шанс схватить блокировку надолго и больше риск случайно сделать лишний SQL, потому что кто‑то внутри транзакции «всего лишь» вызвал size() у коллекции. В итоге вы получаете код, который вроде бы корректный, но по поведению напоминает комнату с котом и пакетом: вроде тихо, но это не значит, что безопасно.

Паттерн smaller unit of work означает, что транзакция должна охватывать только одну связную business‑операцию, а не «операцию плюс отчёт, плюс логирование, плюс форматирование ответа». Самый практичный способ научиться этому — искать места, где внутри @Transactional происходит работа, не требующая транзакции: построение строк, сортировка, группировка, сериализация, подготовка “красивого ответа”.

В Commerce Persistence Lab хороший пример — закрытие заказов и генерация отчёта. Наивно это выглядит так: мы открыли транзакцию, пробежали по списку id, поменяли статус и сразу же собираем строковый отчёт. Формально всё работает. Но транзакция живёт дольше, чем нужно, а persistence context держит managed‑сущности, пока вы форматируете текст. Если в процессе отчёта вы случайно тронете что‑то lazy — получите дополнительные запросы. Если список большой — получите лишнюю нагрузку на dirty checking.

Правильный рефакторинг: транзакция делает только mutation и возвращает минимальный результат, который безопасен вне транзакции. А форматирование отчёта живёт снаружи, где Hibernate уже “не участвует”.

Пример: command‑сервис закрывает заказы и возвращает номера

package com.example.commerce.orders.service;

import com.example.commerce.orders.entity.OrderStatus;
import com.example.commerce.orders.entity.PurchaseOrder;
import com.example.commerce.orders.repository.OrderRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class OrderCloseCommandService {

    private final OrderRepository orderRepository;

    public OrderCloseCommandService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Transactional // Здесь живёт только мутация: меняем статус и сразу выходим из транзакции.
    public List<String> closeOrders(List<Long> ids) {
        return orderRepository.findAllById(ids).stream()
            .peek(o -> o.setStatus(OrderStatus.CLOSED)) // Побочный эффект намеренный: обновляем managed-сущность внутри UoW.
            .map(PurchaseOrder::getOrderNumber) // Возвращаем простое значение, которое безопасно вне транзакции.
            .toList();
    }
}

Смысл в том, что мы возвращаем простые значения, которые уже есть в PurchaseOrder и не требуют lazy‑доступа. Мы не возвращаем List<PurchaseOrder> наружу, потому что это снова откроет дверь entity leakage и случайным подзагрузкам.

Пример: отдельный сервис строит отчёт уже вне транзакции

package com.example.commerce.orders.service;

import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class OrderCloseReportService {

    private final OrderCloseCommandService commandService;

    public OrderCloseReportService(OrderCloseCommandService commandService) {
        this.commandService = commandService;
    }

    public String closeAndBuildReport(List<Long> ids) {
        List<String> numbers = commandService.closeOrders(ids); // Транзакция заканчивается внутри commandService.
        return String.join("\n", numbers); // Форматирование делаем снаружи: Hibernate тут уже "не участвует".
    }
}

Вот это и есть smaller unit of work в самом прикладном виде: транзакция закрывается сразу после mutation‑части, а всё остальное делается без участия Hibernate.

Нюанс, о который часто спотыкаются: self-invocation

Иногда разработчик пытается сделать «один класс, но два метода»: внешний нетранзакционный вызывает внутренний @Transactional. Если вызов происходит внутри одного и того же класса, транзакция может не примениться из‑за прокси‑механики Spring. Мы это уже обсуждали раньше, поэтому здесь просто напоминаю: если хотите реально разделить границы, делайте отдельный бин (как в примере выше) или используйте другие явные механизмы. Не надо устраивать “транзакцию Шрёдингера”, которая есть только в аннотации.

Мини‑схема: «широкий» vs «узкий» unit of work

flowchart TD
    A["@Transactional метод"] --> B["Чтение"]
    B --> C["Изменение"]
    C --> D["Форматирование/логирование/экспорт"]
    D --> E["Commit"]

    A2["@Transactional command"] --> C2["Изменение"]
    C2 --> E2["Commit"]
    E2 --> D2["Форматирование вне транзакции"]

Если смотреть на это как на правило, оно звучит чуть скучно, но в реальном проекте экономит часы диагностики: в транзакции держим только то, что действительно требует транзакции.

5. Сценарий «до/после» в Commerce Persistence Lab

Когда refactoring patterns живут по отдельности, они выглядят как три независимые идеи. На практике они хорошо работают «в связке», потому что каждая закрывает слабое место другой. Давайте соберём небольшую историю на знакомом домене заказов, не превращая её в “перепишем всё приложение”.

Представим use case: «показать администратору таблицу заказов и дать возможность закрыть выбранные». В наивной версии можно сделать один сервис: он загружает PurchaseOrder списком, отдаёт наружу entity, UI где‑то читает customer.email, потом пользователь выбирает несколько заказов, сервис закрывает их и возвращает список закрытых entity.

Проблема в том, что такая версия держится на трёх “авось”: авось никто не тронет лишние lazy‑связи, авось транзакция не окажется шире, чем мы думаем, и авось findById() в цикле не создаст проблему на объёме.

В refactored версии мы делаем скучно, но безопасно.

Список заказов отдаётся как DTO (это read-model split). Репозиторий для списка — query‑ориентированный, и запрос явно фиксирует join к клиенту ровно для email (это explicit query). Сервис чтения помечен @Transactional(readOnly = true) и возвращает список строк, а не entity.

Закрытие заказов делается отдельным command‑сервисом, который в транзакции меняет статус и возвращает только номера закрытых заказов (это smaller unit of work + отказ от entity leakage на выходе). Если нужно показать результат, отдельный слой уже вне транзакции собирает строку отчёта или DTO для ответа.

Важно, что это не требует “архитектурной революции”. Мы не вводим новые технологии, не меняем Hibernate, не переписываем всю доменную модель. Мы просто делаем границы данных явными: что читаем, что пишем, где транзакция начинается и где заканчивается.

И это почти всегда даёт два инженерных эффекта, которые приятно ощущаются даже начинающим.

Первый эффект — SQL становится читаемым. Не «мне кажется, запросов меньше», а реально меньше точек, где запрос может появиться “сам”. Вы читаете DTO — значит, лишний SQL не появится из-за геттера.

Второй эффект — код становится проще для code review. Когда вы видите findForEditingById() или findActiveRows(), вы понимаете намерение. Когда вы видите сервис OrderCloseCommandService, вы понимаете границу мутации. Это очень “немагический” стиль, и он прекрасно сочетается с тем, что Hibernate по природе своей магический. Мы как бы балансируем силы: Hibernate делает неявное внутри unit of work, а мы делаем явным сам unit of work и форму данных.

6. Типичные ошибки при рефакторинге persistence layer

Ошибка №1: заменить giant entity graph на giant DTO и считать, что стало лучше.
Иногда разработчик слышит “read-model split” и решает: “О, значит сделаем один DTO OrderFullDto на 40 полей и будем жить счастливо”. Проблема в том, что вы просто перенесли гигантизм из entity в DTO. Такой DTO начнёт тянуть join’ы, раздувать result set, усложнять запросы и превратится в «универсальный формат на всё». Правильнее делать несколько узких read‑моделей: отдельная для списка, отдельная для карточки, отдельная для экспорта. Это скучно, но стабильно.

Ошибка №2: сделать DTO, но оставить сервис, который возвращает entity “на всякий случай”.
Часто бывает так: кто‑то добавил OrderSummary, но старый метод getOrder() всё равно возвращает PurchaseOrder, потому что “ну иногда кому-то надо”. В результате entity leakage остаётся, и DTO не становится стандартным путём. Важно договориться в проекте: read‑сервисы возвращают read‑модели, а entity остаются внутри persistence/write‑слоя. Если “иногда надо”, это обычно означает “у нас два разных use case, которые мы случайно смешали”.

Ошибка №3: пытаться «сузить unit of work», но оставить внутри транзакции форматирование, сортировки и “красивые” маппинги.
Сужение транзакции — это не про то, чтобы поставить аннотацию @Transactional на другой метод и успокоиться. Это про то, чтобы вынести из транзакции всё, что не требует транзакции. Если внутри @Transactional вы строите JSON, CSV, огромные строки, делаете сложные группировки — транзакция всё равно широкая. Hibernate всё равно живёт долго. Лучше возвращать из транзакции минимальные данные (ID, номера, суммы), а “красоту” строить снаружи.

Ошибка №4: писать явные запросы, но не отражать use case в названии метода.
Можно сделать @Query и назвать метод findSomethingCool(). Формально это explicit query, но по ощущениям — снова магия. Хорошее имя метода — часть предсказуемости. findForEditingById(), findActiveRows(), findSummaryById() звучат чуть многословно, но они объясняют intent и помогают не использовать метод “не по назначению”.

Ошибка №5: чинить только код, игнорируя SQL trace и тесты.
Persistence layer обманчив: после рефакторинга тесты могут оставаться зелёными, а SQL поведение — всё ещё быть проблемным (например, стало меньше запросов, но шире join, или ушёл N+1, но появился accidental update). Поэтому после каждого refactoring move важно хотя бы раз посмотреть SQL trace: сколько запросов, какой shape, нет ли лишних UPDATE. Мы не устраиваем performance‑аудит на каждый коммит, но “посмотреть глазами” — это базовая гигиена.

1
Задача
Hibernate deep-dive, 29 уровень, 3 лекция
Недоступна
Список активных товаров как read-model
Список активных товаров как read-model
1
Задача
Hibernate deep-dive, 29 уровень, 3 лекция
Недоступна
Явный запрос для сценария утверждения заявки
Явный запрос для сценария утверждения заявки
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ