Реализация подтипа подтипа в шаблоне проектирования типа / подтипа с взаимоисключающими подклассами


20

Вступление

Чтобы этот вопрос был полезным для будущих читателей, я буду использовать общую модель данных для иллюстрации проблемы, с которой я сталкиваюсь.

Наша модель данных состоит из 3 объектов, которые должны быть помечены как A, Bи C. Для простоты все их атрибуты будут иметь intтип.

Entity Aимеет следующие атрибуты: D, Eи X;

Entity Bимеет следующие атрибуты: D, Eи Y;

Сущность Cимеет следующие атрибуты: Dи Z;

Поскольку все объекты имеют общий атрибут D, я решил применить дизайн типа / подтипа .

Важно: объекты являются взаимоисключающими! Это означает, что объектом является либо А, либо В, либо С.

Проблема:

Объекты Aи Bимеют еще один общий атрибут E, но этот атрибут отсутствует в объекте C.

Вопрос:

Я хотел бы использовать описанную выше характеристику для дальнейшей оптимизации моей конструкции, если это возможно.

Если честно, я понятия не имею, как это сделать, и с чего начать, отсюда и этот пост.

Ответы:


6

Поскольку этот Вопрос является продолжением « Правильна ли моя реализация шаблона проектирования типа / подтипа (для взаимоисключающих подклассов)? , что само по себе является продолжением Не знаю, как преобразовать переменную сущность в реляционную таблицу , я бы спросил: что именно вы пытаетесь оптимизировать? Место хранения? Объектная модель? Сложность запроса? Запрос производительности? При оптимизации одного аспекта по сравнению с другим существуют компромиссы, поскольку нельзя оптимизировать все аспекты одновременно.

Я полностью согласен с мнениями Ремуса относительно:

  • У каждого подхода есть свои плюсы и минусы (то есть постоянный фактор «зависит»), и
  • Первым приоритетом является эффективность модели данных (неэффективная модель данных не может быть исправлена ​​чистым и / или эффективным кодом приложения)

Тем не менее, перед вами стоит выбор между следующим, упорядоченным в порядке наименьшей нормализации к большей части нормализации:

  • продвижение собственности Eв таблицу базового типа
  • держать его в нескольких таблицах подтипов
  • полностью нормализуется Eдо новой промежуточной таблицы подклассов на том же уровне C, что Aи Bнепосредственно будет подклассами ( @ ответ MDCCL )

Давайте посмотрим на каждый вариант:

Переместить свойство Eв таблицу базового типа

PROs

  • Снижение сложности запросов для запросов, которые нуждаются, Eно не требуют X, Yили Z.
  • Потенциально более эффективен для запросов, которые нуждаются, Eно не нужны X, Yили Z(особенно в совокупности запросов) из-за отсутствия JOIN.
  • Потенциал для создания индекса (D, E)(и, если да, потенциально фильтрованного индекса, (D, E)где EntityType <> C, если такое условие разрешено)

ПРОТИВ

  • Не могу отметить EкакNOT NULL
  • Требуется дополнительная CHECK CONSTRAINTтаблица базового типа, чтобы убедиться, что E IS NULLкогда EntityType = C(хотя это не большая проблема)
  • Необходимо информировать пользователей модели данных о том, почему это Eнеобходимо NULLи даже следует полностью игнорировать, когда EntityType = C.
  • Немного менее эффективно, когда Eиспользуется тип с фиксированной длиной, и большая часть строк предназначена для типа EntityType C(т.е. не использует, Eследовательно, он есть NULL) и не использует ни SPARSEпараметр в столбце, ни сжатие данных в кластеризованном индексе.
  • Потенциально менее эффективен для запросов, которые не нужны, Eпоскольку присутствие Eв таблице базового типа увеличит размер каждой строки, что, в свою очередь, уменьшит количество строк, которые могут поместиться на странице данных. Но это сильно зависит от точного типа данных E, FILLFACTOR, количества строк в таблице базового типа и т. Д.

Сохранить свойство Eв каждой таблице подтипа

PROs

  • Более чистая модель данных (т. Е. Не нужно беспокоиться о том, чтобы объяснить другим, почему столбец Eв таблице базового типа не следует использовать, потому что «его действительно нет»)
  • Вероятно, более близко напоминает объект-модель
  • Может пометить столбец как NOT NULLобязательное свойство объекта
  • Нет необходимости в дополнительных CHECK CONSTRAINTтаблицах базового типа, чтобы гарантировать, что E IS NULLкогда EntityType = C(хотя это не огромный выигрыш)

ПРОТИВ

  • Требуется ПРИСОЕДИНИТЬСЯ к подтипу Table (s), чтобы получить это свойство
  • Потенциально немного менее эффективно при необходимости E, благодаря JOIN, в зависимости от того, сколько строк A+ у Bвас, а не сколько строк Cесть.
  • Немного сложнее / сложнее для операций, которые имеют дело только с сущностями Aи Bне C ) как с одним и тем же «типом». Конечно, вы можете абстрагировать это через представление, которое выполняет UNION ALLмежду a SELECTиз таблиц JOINed для Aи другой SELECTиз таблиц JOINed для B. Это позволит снизить сложность запросов SELECT , но не так полезно для INSERTи UPDATEзапросов.
  • В зависимости от конкретных запросов и от того, как часто они выполняются, это может быть потенциальной неэффективностью в тех случаях, когда наличие индекса (D, E)действительно поможет одному или нескольким часто используемым запросам, поскольку они не могут быть проиндексированы вместе.

Нормализовать Eдо промежуточной таблицы между базовым классом и A&B

(Обратите внимание, что мне нравится ответ @ MDCCL как жизнеспособной альтернативы, в зависимости от обстоятельств. Следующее не предназначено для строгой критики этого подхода, а как средство добавления некоторой перспективы - моей, конечно, - путем оценки это в том же контексте, что и два варианта, которые я уже предложил. Это облегчит прояснение того, что я вижу как относительную разницу между полной нормализацией и текущим подходом частичной нормализации.)

PROs

  • модель данных полностью нормализована (в этом не может быть ничего плохого, учитывая, что именно для этого предназначены СУБД)
  • уменьшенная сложность запросов для запросов, нуждающихся Aи B, но не C(то есть нет необходимости в двух запросах, соединенных через UNION ALL)

ПРОТИВ

  • немного больше занимаемого места ( Barтаблица дублирует идентификатор, и есть новый столбец BarTypeCode) [незначительно, но кое-что нужно знать]
  • Небольшое увеличение сложности запроса, так как требуется дополнительное, JOINчтобы добраться до AилиB
  • увеличенная площадь поверхности для блокировки, в основном включенная INSERT( DELETEможет обрабатываться неявно через пометку внешних ключей как ON CASCADE DELETE), поскольку транзакция будет оставаться открытой на таблице базового класса немного дольше (т. е. Foo) [незначительно, но кое-что следует знать]
  • нет непосредственного знания фактического типа - Aили B- в Таблице базового класса Foo; она знает только тип , Brкоторый может быть Aили B:

    Это означает, что если вам нужно выполнить запросы к общей базовой информации, но нужно либо классифицировать по типу сущности, либо отфильтровать один или несколько типов сущностей, то в таблице базового класса недостаточно информации, и в этом случае вам нужно стол. Это также снизит эффективность индексации столбца.LEFT JOINBarFooTypeCode

  • нет единого подхода к взаимодействию с A& Bvs C:

    Это означает, что если каждая сущность имеет непосредственное отношение к таблице базового класса, так что существует только одно соединение, чтобы получить полную сущность, то каждый может быстрее и проще создать знакомство с точки зрения работы с моделью данных. Там будет общий подход к запросам / хранимым процедурам, что позволит им быстрее разрабатывать и с меньшей вероятностью иметь ошибки. Последовательный подход также позволяет быстрее и проще добавлять новые подтипы в будущем.

  • потенциально менее адаптируемый к бизнес-правилам, которые меняются со временем:

    Это означает, что вещи всегда меняются, и довольно легко перейти Eк Таблице базового класса, если она станет общей для всех подтипов. Также достаточно легко перенести общее свойство в подтипы, если изменения в природе сущностей делают это целесообразным изменением. Достаточно просто разбить подтип на два подтипа (просто создать другое SubTypeIDзначение) или объединить два или более подтипа в один. И наоборот, что, если Eвпоследствии оно станет общим свойством всех подтипов? Тогда промежуточный слой Barтаблицы будет бессмысленным, а сложность не будет того стоить. Конечно, невозможно знать, произойдет ли такое изменение через 5 или даже 10 лет, поэтому Barтаблица не обязательнои даже весьма вероятно, что это плохая идея (именно поэтому я сказал « потенциально менее адаптируемый»). Это просто моменты для рассмотрения; это игра в любом направлении.

  • потенциально неуместная группировка:

    Значение, только потому , что Eсобственность поделена между типами сущностей Aи Bне означает , что Aи B должны быть сгруппированы вместе. То, что вещи «выглядят» одинаково (т.е. имеют одинаковые свойства), не означает, что они одинаковы.

Резюме

Точно так же, как принятие решения о том, когда / когда проводить денормализацию, наилучший подход к этой конкретной ситуации зависит от рассмотрения следующих аспектов использования модели данных и обеспечения того, чтобы выгоды перевешивали затраты:

  • сколько строк у вас будет для каждого EntityType (посмотрите, по крайней мере, на 5 лет вперед, предполагая рост выше среднего)
  • Сколько ГБ будет каждая из этих таблиц (базовый тип и подтипы) будет через 5 лет?
  • какой конкретный тип данных является свойством E
  • это только одно свойство или есть несколько или даже несколько свойств
  • какие запросы вам понадобятся Eи как часто они будут выполняться
  • какие запросы вам понадобятся, которые не нужны Eи как часто они будут выполняться

Я думаю, что по умолчанию склоняюсь к хранению Eв отдельных таблицах подтипов, потому что это, по крайней мере, «чище». Я хотел бы рассмотреть переход Eк таблице базового типа IF: большинство строк были не для EntityType of C; и количество рядов было по крайней мере в миллионах; и я выполняю запросы чаще, чем не выполняемые, Eи / или запросы, которые выиграют от индекса, (D, E)либо выполняются очень часто, и / или требуют достаточно системных ресурсов, так что наличие индекса уменьшает общее использование ресурсов или, по крайней мере, предотвращает скачки потребления ресурсов превышают допустимые уровни или длятся достаточно долго, чтобы вызвать чрезмерную блокировку и / или увеличение взаимоблокировок.


ОБНОВИТЬ

ОП прокомментировал этот ответ, что:

Мои работодатели изменили бизнес-логику, полностью исключив E!

Это изменение особенно важно, потому что именно то, что я предсказал, может произойти в подразделе «CON» в разделе «Нормализовать Eдо промежуточной таблицы между базовым классом и разделом A& B» выше (6-й пункт). Конкретная проблема заключается в том, насколько легко / сложно провести рефакторинг модели данных, когда такие изменения происходят (и они всегда происходят). Некоторые утверждают, что любая модель данных может быть реорганизована / изменена, поэтому начните с идеала. Но хотя на техническом уровне верно то, что все может быть реорганизовано, реальность ситуации зависит от масштаба.

Ресурсы не бесконечны, не только процессор / диск / оперативная память, но и ресурсы для разработки: время и деньги. Компании постоянно устанавливают приоритеты для проектов, потому что эти ресурсы очень ограничены. И довольно часто (по крайней мере, по моему опыту) проекты, направленные на повышение эффективности (даже как производительности системы, так и ускорения разработки / уменьшения количества ошибок), имеют приоритет перед проектами, которые повышают функциональность. В то время как это разочаровывает нас, технических специалистов, потому что мы понимаем, каковы долгосрочные выгоды от проектов рефакторинга, просто характер бизнеса заключается в том, что менее техническим специалистам легче увидеть прямую связь между новой функциональностью и новыми доход. Это сводится к следующему: «мы вернемся, чтобы исправить это позже» ==

Имея это в виду, если размер данных достаточно мал, чтобы можно было вносить изменения по самому запросу, и / или у вас есть окно обслуживания, которое достаточно длинное, чтобы не только вносить изменения, но и откатываться, если что-то происходит неверно, тогда нормализация Eдо промежуточной таблицы между таблицей базового класса и таблицами A& Bподкласса может сработать (хотя это все равно оставляет вас без непосредственных знаний о конкретном типе ( AилиB) в таблице базового класса). НО, если у вас есть сотни миллионов строк в этих таблицах и невероятное количество кода, ссылающегося на таблицы (код, который должен быть проверен при внесении изменений), то обычно стоит быть более прагматичным, чем идеалистическим. И это среда, с которой мне приходилось сталкиваться годами: 987 миллионов строк и 615 ГБ в таблице базового класса, распределенных по 18 серверам. И так много кода попало в эти таблицы (таблицы базового класса и подкласса), что имело место большое сопротивление - в основном со стороны руководства, а иногда и всей команды - к внесению каких-либо изменений в связи с объемом разработки и Ресурсы QA, которые должны быть выделены.

Итак, еще раз, «лучший» подход может быть определен только от ситуации к ситуации: вам нужно знать свою систему (то есть, сколько данных и как все таблицы и код связаны), как выполнить рефакторинг, и люди с которым вы работаете (ваша команда и, возможно, руководство - можете ли вы получить их взнос для такого проекта?). Есть некоторые изменения, которые я упоминал и планировал в течение 1-2 лет, и мне потребовалось несколько спринтов / релизов, чтобы реализовать 85% из них. Но если у вас есть только <1 миллион строк и не много кода, привязанного к этим таблицам, то вы, вероятно, сможете начать с более идеальной / «чистой» стороны вещей.

Просто помните, какой бы путь вы ни выбрали, обратите внимание на то, как эта модель будет работать как минимум в течение следующих 2 лет (если это возможно). Обращайте внимание на то, что сработало и что вызвало боль, даже если в то время это казалось величайшей идеей (а это значит, что вы также должны позволить себе принять провал - мы все это делаем - чтобы вы могли честно оценить болевые точки ). И обратите внимание на то, почему определенные решения сработали или не сработали, чтобы вы могли принимать решения, которые с большей вероятностью будут «лучше» в следующий раз :-).


17

По словам Мартина Фаулера, существует три подхода к проблеме наследования таблиц:

  • Наследование одной таблицы: одна таблица представляет все типы. Неиспользуемые атрибуты имеют значение NULL.
  • Наследование конкретной таблицы : одна таблица для каждого конкретного типа, столбцы каждой таблицы для каждого атрибута типа. Нет связи между таблицами.
  • Наследование таблиц классов : одна таблица для каждого типа, каждая таблица имеет атрибуты только для новых, не унаследованных атрибутов. Таблицы связаны, отражая фактическую иерархию наследования типов.

Вы можете начать с них в качестве отправной точки для поиска плюсов и минусов каждого подхода. Суть в том, что все подходы имеют серьезные недостатки, и ни один не имеет подавляющего преимущества. Более известная как несоответствие реляционного импеданса объекта , эта проблема еще не найдена.

Лично я считаю, что тип проблем, к которым может привести плохой реляционный дизайн, на несколько порядков серьезнее, чем тип проблем, возникающих из-за плохого типового дизайна. Плохое проектирование базы данных приводит к медленным запросам, аномалиям обновления, увеличению размера данных, взаимным блокировкам и не отвечающим приложениям, а также к потере десятков и сотен гигабайт данных в неправильном формате . Плохой дизайн шрифта приводит к тому, что трудно поддерживать и обновлять код , а не время выполнения. Поэтому в моей книге правильный реляционный дизайн превосходит любую чистоту ОО-типа снова и снова.


@AlwaysLearningNewStuff Я думаю, что этот вопрос является продолжением на dba.stackexchange.com/questions/139092 , верно? В реализации есть у вас сделать есть наследование таблицы.
Ремус Русану

Да, прежде чем задавать этот вопрос, я хотел убедиться, что я правильно понял, как сначала реализовать дизайн типа / подтипа. Теперь я сталкиваюсь с вышеописанной проблемой, когда некоторые (но не все!) Подклассы имеют общие атрибуты. Мне было интересно, могу ли я что-то сделать для оптимизации модели данных в этом случае, вместо того, чтобы игнорировать этот нюанс ...
AlwaysLearningNewStuff

6

Согласно моей интерпретации ваших спецификаций, вы хотите найти метод для реализации двух разных (но связанных ) структур супертип-подтип .

Чтобы раскрыть подход для достижения вышеупомянутой задачи, я собираюсь добавить к рассматриваемому сценарию два классических гипотетических типа сущностей, называемых Fooи Bar, которые я подробно опишу ниже.

Бизнес правила

Вот несколько утверждений, которые помогут мне создать логическую модель:

  • A Foo is either one Bar or one C
  • A Foo is categorized by one FooType
  • A Bar is either one A or one C
  • A Bar is classified by one BarType

Логическая модель

И затем, результирующая логическая модель IDEF1X [1] показана на рисунке 1 (и вы также можете загрузить ее из Dropbox в формате PDF ):

Рисунок 1 - Модель данных гипотетических отношений супертип-подтип

Дополнение Foo and Bar

Я не добавлял Fooи Barчтобы модель выглядела лучше, но чтобы она была более выразительной. Я считаю, что они важны из-за следующего:

  • Как Aи Bразделить названный атрибут E, эта особенность предполагает, что они являются типами субъективности отдельного (но связанного) вида концепции , события , личности , измерения и т. Д., BarКоторые я представлял с помощью типа превосходства, который, в свою очередь, является тип подчиненности Foo, который содержит Dатрибут в верхней части иерархии.

  • Поскольку Cтолько один атрибут разделяет с остальными обсуждаемыми типами сущностей, т. Е. DЭтот аспект намекает на то, что это тип сущности другого типа концепции , события , лица , измерения и т. Д., Поэтому я изобразил это обстоятельство в силу Fooсупер тип объекта.

Тем не менее, это всего лишь предположения, и поскольку реляционная база данных предназначена для точного отражения семантики определенного бизнес-контекста , вам необходимо идентифицировать и классифицировать все вещи, представляющие интерес в вашей конкретной области, чтобы вы могли, точно, захватить больше смысла ,

Важные факторы на этапе проектирования

Весьма полезно осознавать тот факт, что, исключая всю терминологию, исключительный кластер супертип-подтип является обычным отношением. Опишем ситуацию следующим образом:

  • Каждое исключительное вхождение типа суперчувствительности связано только с одним дополнением типа субэлемента .

Таким образом, в этих случаях есть соответствие (или количество элементов) один к одному (1: 1).

Как вы знаете из предыдущих постов, атрибут дискриминатора (столбец, если он реализован) играет первостепенную роль при создании ассоциации такого рода, поскольку он указывает на правильный экземпляр подтипа, с которым связан супертип . Миграция первичного ключа из (I) супертипе к (б) подтипы также имеет первостепенное значение.

Бетонная конструкция DDL

А затем я написал структуру DDL, основанную на логической модели, представленной выше:

CREATE TABLE FooType -- Look-up table.
(
    FooTypeCode     CHAR(2)  NOT NULL,
    Description     CHAR(90) NOT NULL, 
    CreatedDateTime DATETIME NOT NULL,
    CONSTRAINT PK_FooType             PRIMARY KEY (FooTypeCode),
    CONSTRAINT AK_FooType_Description UNIQUE      (Description)
);

CREATE TABLE Foo -- Supertype
(
    FooId           INT      NOT NULL, -- This PK migrates (1) to ‘Bar’ as ‘BarId’, (2) to ‘A’ as ‘AId’, (3) to ‘B’ as ‘BId’, and (4) to ‘C’ as ‘CId’.
    FooTypeCode     CHAR(2)  NOT NULL, -- Discriminator column.
    D               INT      NOT NULL, -- Column that applies to ‘Bar’ (and therefore to ‘A’ and ‘B’) and ‘C’.
    CreatedDateTime DATETIME NOT NULL,
    CONSTRAINT PK_Foo                 PRIMARY KEY (FooId),
    CONSTRAINT FK_from_Foo_to_FooType FOREIGN KEY (FooTypeCode)
        REFERENCES FooType (FooTypeCode)
);

CREATE TABLE BarType -- Look-up table.
(
    BarTypeCode CHAR(1)  NOT NULL,  
    Description CHAR(90) NOT NULL,  
    CONSTRAINT PK_BarType             PRIMARY KEY (BarTypeCode),
    CONSTRAINT AK_BarType_Description UNIQUE      (Description)
);

CREATE TABLE Bar -- Subtype of ‘Foo’.
(
    BarId       INT     NOT NULL, -- PK and FK.
    BarTypeCode CHAR(1) NOT NULL, -- Discriminator column. 
    E           INT     NOT NULL, -- Column that applies to ‘A’ and ‘B’.
    CONSTRAINT PK_Bar             PRIMARY KEY (BarId),
    CONSTRAINT FK_from_Bar_to_Foo FOREIGN KEY (BarId)
        REFERENCES Foo (FooId),
    CONSTRAINT FK_from_Bar_to_BarType FOREIGN KEY (BarTypeCode)
        REFERENCES BarType (BarTypeCode)    
);

CREATE TABLE A -- Subtype of ‘Bar’.
(
    AId INT NOT NULL, -- PK and FK.
    X   INT NOT NULL, -- Particular column.  
    CONSTRAINT PK_A             PRIMARY KEY (AId),
    CONSTRAINT FK_from_A_to_Bar FOREIGN KEY (AId)
        REFERENCES Bar (BarId)  
);

CREATE TABLE B -- (1) Subtype of ‘Bar’ and (2) supertype of ‘A’ and ‘B’.
(
    BId INT NOT NULL, -- PK and FK.
    Y   INT NOT NULL, -- Particular column.  
    CONSTRAINT PK_B             PRIMARY KEY (BId),
    CONSTRAINT FK_from_B_to_Bar FOREIGN KEY (BId)
        REFERENCES Bar (BarId)  
);

CREATE TABLE C -- Subtype of ‘Foo’.
(
    CId INT NOT NULL, -- PK and FK.
    Z   INT NOT NULL, -- Particular column.  
    CONSTRAINT PK_C             PRIMARY KEY (CId),
    CONSTRAINT FK_from_C_to_Foo FOREIGN KEY (FooId)
        REFERENCES Foo (FooId)  
);

С этой структурой вы избегаете хранения меток NULL в ваших базовых таблицах (или отношениях ), что внесет неоднозначность в вашу базу данных.

Целостность, последовательность и другие соображения

После того, как вы реализуете свою базу данных, вы должны убедиться, что (a) каждая исключительная строка супертипа всегда дополняется соответствующим аналогом подтипа и, в свою очередь, гарантировать, что (b) такая строка подтипа совместима со значением, содержащимся в столбце дискриминатора супертипа. , Поэтому довольно удобно использовать ACID TRANSACTIONS, чтобы убедиться, что эти условия выполняются в вашей базе данных.

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

Два ранее опубликованных ответа уже включают соответствующие пункты, которые, безусловно, стоит учитывать при проектировании, создании и управлении вашей базой данных и ее прикладными программами.

Получение данных с помощью определений VIEW

Вы можете настроить некоторые представления, которые объединяют столбцы разных групп подтипов супертипов , так что вы можете извлекать данные под рукой, например, не каждый раз записывая необходимые предложения JOIN. Таким образом, вы можете легко ВЫБРАТЬ непосредственно ИЗ ВИДА ( производного отношения или таблицы ), представляющего интерес.

Как видите, «Тед» Кодд был, несомненно, гением. Инструменты, которые он завещал, довольно сильны и элегантны, и, конечно же, хорошо интегрированы друг с другом.

Связанные ресурсы

Если вы хотите проанализировать какую-то обширную базу данных, которая включает отношения супертип-подтип, вы найдете полезными необычные ответы, предложенные @PerformanceDBA на следующие вопросы переполнения стека:


Заметка

1. Определение интеграции для информационного моделирования ( IDEF1X ) - это очень рекомендуемый метод моделирования данных, который был установлен в качестве стандарта в декабре 1993 года Национальным институтом стандартов и технологий США ( NIST ). Он основывается на (а) раннем теоретическом материале, написанном д-ром Э. Ф. Коддом; на (б) в сущности-связи с учетом данных, разработанного д - ром П. Ченом ; а также о (c) методике проектирования логических баз данных, созданной Робертом Г. Брауном. Стоит отметить, что IDEF1X был формализован с помощью логики первого порядка.


Мои работодатели изменили бизнес-логику, Eвообще убрав ! Причина для того, чтобы принять ответ пользователя srutzky , состоит в том, что он дает хорошие моменты, которые помогают мне принять решение о выборе наиболее эффективного маршрута. Если бы не это, я бы принял ваш ответ. Я проголосовал за ваш ответ ранее. Еще раз спасибо!
AlwaysLearningNewStuff
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.