crud operations что это

Что такое CRUD-операции

Если вы когда-либо работали с базами данных, вы, вероятно, использовали операции CRUD. CREATE, READ, UPDATE и DELETE — это четыре основные операции программирования РСУБД. Операции CRUD используются для управления, чтения, вставки, удаления и редактирования данных таблицы.

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

Что такое операции CRUD?

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

CRUD (создание, чтение, обновление, удаление) — это аббревиатура, обозначающая четыре функции, которые мы используем для реализации приложений постоянного хранения и приложений реляционных баз данных, включая Oracle Database, Microsoft SQL Server и MySQL.

В таблице ниже показано, что означает каждая операция CRUD.

Письмо Операция Функция
C Создавать Вставлять
р Читать Выбирать
U Обновлять Редактировать
D Удалить Удалить

Для SQL карты CRUD для вставки, выбора, обновления и удаления соответственно. Такие операции, как управление безопасностью, управление транзакциями, доступ и разрешение, а также оптимизация производительности, основаны на CRUD.

Почему CRUD так важен?

CRUD постоянно используется для всего, что связано с базами данных и проектированием баз данных. Разработчики программного обеспечения ничего не могут сделать без операций CRUD. Например, при разработке веб-сайтов используется REST (передача репрезентативного состояния), который является надмножеством CRUD, используемого для ресурсов HTTP.

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

CRUD предлагает множество преимуществ, в том числе:

CREATE

Create позволяет добавлять новые строки в вашу таблицу. Вы можете сделать это с помощью команды INSERT INTO. Команда начинается с INSERT INTOключевого слова, за которым следует имя таблицы, имена столбцов и значения, которые нужно вставить.

При использовании у INSERT INTOвас есть два варианта:

В приведенном ниже примере мы добавим данные в нашу таблицу пекарни.

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

Функция чтения похожа на функцию поиска, поскольку позволяет извлекать определенные записи и считывать их значения. Читать относится кSELECT

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

Это не внесет никаких изменений в таблицу меню, а просто отобразит все записи в этой таблице.

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

UPDATE

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

Рекомендуется ограничить количество строк, так как это помогает избежать проблем с параллелизмом.

Чтобы обновить существующую запись, используйте следующее:

Допустим, мы хотим обновить название и цену товара. Мы бы использовали:

Это обновит таблицу, так что предыдущая запись с id 1теперь будет заменена шоколадным круассаном с price 2.5.

DELETE

Удалить используется для удаления записи из таблицы. SQL и имеет встроенную функцию удаления для одновременного удаления одной или нескольких записей из базы данных. Некоторые приложения реляционных баз данных могут разрешать жесткое удаление (безвозвратное удаление) или мягкое удаление (обновление статуса строки).

Команда удаления выглядит следующим образом:

Если мы хотим удалить один элемент из таблицы, мы используем:

Это приведет к удалению строки с хлебным предметом из таблицы. Если вы хотите удалить все записи из таблицы, вы можете использовать:

Следующие шаги для вашего обучения

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

Если вы хотите узнать больше о SQL, вы можете начать со следующих тем:

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

Источник

Технология CRUD-матрицы. Практический опыт


Технология CRUD-матрицы — это хороший инструмент для каждого члена Agile-команды на протяжении всего жизненного цикла продукта. CRUD-матрица позволяет наладить адекватный диалог с клиентом и выявить дублирование функционала, а также устранить противоречивость модели. Что касается оценки времени, то в этом моменте CRUD-матрица значительно уступает такому инструменту, как “planning poker”, который позволяет провести адекватную оценку с учетом объективных причин.

Немного теории: описание методики

IIBA в компетенцию бизнес-аналитика в области Agile (далее Agile-аналитик) относит следующие технологии:
— Определение критериев оценки и приёмки;
— Мозговой штурм;
— Различные методы оценки (метод Delphi, параметрический метод, метод аналогий, трёхточечный метод и т.п.);
— Прототипирование;
— Разработка сценариев и описание прецедентов;
— Моделирование областей для анализа или поставки решений;
— Пользовательские истории.

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

Практическая часть: разработка CRUD-матрицы

Разработка CRUD-матрицы помогает Agile-команде сконцентрироваться на существенных прецедентах, описывающих бизнес-процесс. CRUD-матрица формируется в виде таблицы, в которой в верхней части перечисляются все классы из диаграммы классов, а в левой части отражается список прецедентов. Задача Agile-аналитика заполнить пересечения между прецедентами и классами следующими комбинациями доступа к экземплярам классов: создание (Create), чтение (Read), обновление (Update) или удаление (Delete).
Таким образом описываются все прецеденты, которые выполняют создание, чтение, обновление или удаление одного или нескольких экземпляров класса.
В качестве примера приведу опыт построение CRUD-матрицы в 2007-2008гг. при планировании разработки автоматизированной библиотечной информационной системы для научно-технической библиотеки (далее АБИС НТБ).

Анализ CRUD-матрицы производится в семь шагов:
1.Проверка полноты построения модели
2.Определение зависимостей
3.Определение пакета типовых работ для разработки
4.Оценка времени, необходимого для разработки
5.Проверка модели на непротиворечивость
6.Определение последующей работы и дополнений функционала
7.Определение приоритетов для разработки и поставки

Шаг 1. Проверка полноты построения модели

На основе CRUD-матрицы проверяется полнота построения модели на предмет целесообразности использования классов или прецедентов в разрабатываемой системе.
К примеру, в рамках перечисленных прецедентов не описано создание или удаление класса “Книжный формуляр”. Данный факт может конечно говорить о том, что “Книжный формуляр” создается и удаляется вне разрабатываемой системы или же о том, что Agile-аналитик не описал прецедент “регистрация книги” или “списание книги”, а для этого нужно ввести еще один класс “инвентарная книга”.

Шаг 2. Определение зависимостей

При планировании процесса разработки системы CRUD-матрица помогает определить перечень классов, которые разрабатываются в первую очередь, чтобы покрыть максимальное количество прецедентов. Например, для разработки АБИС НТБ прецедент “регистрация читателя” будет реализован первым, потому что класс читатель согласно CRUD-матрицы используется в 8 прецедентах, в отличие от класса “бронь”. Таким образом определяются нужные и актуальные данные.

Шаг 3. Определение пакета типовых работ для разработки

CRUD-матрица помогает определить типовую реализацию и выявить дублирование функционала в системе. К примеру, класс “книжный формуляр” реализуется по типу класса “библиотекарь” и соответственно времени на реализацию класса “книжный формуляр” потребуется меньше. Таким образом, сокращается время разработки системы.

Шаг 4. Оценка времени, необходимого для разработки

CRUD-матрица предоставляет Agile-команде простой механизм для оценки времени, необходимого для разработки и тестирования определенной части функциональности.
В первом приближении производится оценка каждой комбинации доступа, а затем производится оценка каждого прецедента. К примеру, создание нового экземпляра класса требуется в 4 прецедентах, чтение информации об экземпляре класса в 7 случаях, в 16 случаях требуется обновлять информацию экземпляра класса, а в 6 случаях необходимо удалять экземпляр класса. Используя плановое время на разработку каждой комбинации доступа формируется таблица сложности реализации каждого класса, а также системы в целом:

В случае необходимости, аналитик может ранжировать не только сложность реализации каждого класса, но и каждого прецедента.
Что касается 53 дней, то этот срок уменьшается за счет типовых работ, а именно реализации классов “библиотекарь” и “книжный формуляр”. Для обеспечения более реалистичной оценки на будущее можно использовать фактические значения собранные на основе статистического анализа процессов реализации подобного функционала в предыдущих проектах. В любом случае, последнее слово должно быть за Agile-командой, ведь никакая статистика не поможет в разработке системы, а иногда даже мешает Agile-команде корректно оценить свои возможности.

Шаг 5. Проверка модели на непротиворечивость

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

Шаг 6. Определение последующей работы и дополнений функционала

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

Шаг 7. Определение приоритетов для разработки и поставки

CRUD-матрица позволяет клиентам адекватно оценить приоритеты в реализации какой-либо функциональности, и не позволит переместить её реализацию на следующую итерацию. Например, прецеденты “выдача читательского билета” и “перерегистрация читателя” не могут быть поставлены полностью до тех пор пока не будут разработаны классы: читатель, читательский билет и библиотекарь.

Технология CRUD-матрицы — это хороший инструмент для каждого члена Agile-команды на протяжении всего жизненного цикла продукта. CRUD-матрица позволяет наладить адекватный диалог с клиентом (заказчиком) и выявить дублирование функционала, а также устранить противоречивость модели. Что касается оценки времени, то в этом моменте CRUD-матрица значительно уступает такому инструменту, как “planning poker”, который позволяет провести адекватную оценку с учетом объективных причин. Что касается комбинаций доступа к экземплярам класса, то тут не обязательно использовать именно CRUD-комбинацию, возможны и другие комбинации, например такие: REST, RESTful, GET-PUT-POST-DELETE и др.

Источник

REST/CRUD. Я неправильно его готовлю?

Вступление

Must have для всех ресурсов

Важное отличие slug от id

А теперь принимаем за аксиому «все объекты должны иметь указаные свойства», и, чтоб не размазываться по объекту запихиваем их например в свойство «__link__»:

Кстати, это — валидный объект. Он мог вернуться с какого нибудь url (не «localhost/api/users»). Разработчик, получив такой объект, должен запросить данные по url/urn из __link__, но об этом позже.
Кстати, «по правильному» — не вносить доп. свойство «__lnk__», а использовать для этих целей заголовки формата X-URI, X-URN, X-URL, etc, но не всем веберам нравится работать с заголовками. Мол взял body и побежали.
Хммм… а сколько реализаций REST API имеет подобные свойства? Может я изобрел велосипед?

CRUD/Read или HTTP/GET

Тут все просто. Берем URL, запрашиваем объект, и… А что «и»? И иногда наблюдаем вот такой ответ:
Пример:

Ответ должен быть таким:

При этом count должен жить в HTTP/HEAD, т.к. не имеет на прямую отношения к ресурсу, а характеризует состояние коллекции. Опят же, Вам надо вывести кол-во пользователей. Как быть? Выбирать все и считать в JavaScript? Писать новый url для получения кол-ва элементов? Зачем? Сделайте HTTP/HEAD запрос к колекции. Данных не будет, вернутся только зголовки.

Скептики скажут: для отрисовки всех элементов придется делать множество запросов к бекенду,
Отвечу: HTTP/1.1/Keep-alive спасет нас всех. При использовании javascript-фреймворков и кеширования — большинство данных будут запрошены при инициализации, разово, а после — будут только обмениваться запросами с ответом HTTP/304 (ресурс не изменился).

Знаете почему веберам не нравится такой подход? Им приходится контролировать получение данных с сервера для нормального отображения.
Например: Если данные прилетают сразу со свойствами — можно сразу отрисовать несколько дивов в цикле не заботясь о синхронизации. Если же делать 4 запроса для вывода 4-строк с пользователями — надо как-то отслеживать состояние всех запросов. Т.е. пока ВСЕ запросы не отработают — ничего не рисовать. См. Promise и Future для решения подобных проблем. Они все имеет код для принудительной синхронизации, хотя я бы рисовал сразу как есть…

Источник

CRUD Operations Using the Generic Repository Pattern and Unit of Work in MVC

От переводчика

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

Вступление

Сегодня познакомимся с паттерном «Общий репозиторий» (Generic Repository) и подходом Unit of Work в приложениях ASP.NET MVC. Для этого создадим приложение «Книги», в котором будут реализованы операции создания, получения, обновления и удаления данных. Для упрощения понимания материала пример будет построен на использовании только одной сущности книги.

В предыдущей статье «CRUD Operations Using the Repository Pattern in MVC» мы уже создавали приложение с использованием репозитория для сущности книги, реализующее CRUD операции. Но если мы захотим создать подобное приложение, но уже корпоративного уровня, то нам придется создавать по репозиторию на каждую новую сущность. Проще говоря, мы будем дублировать код, что идет в разрез с подходом DRY — поэтому необходимо воспользоваться паттерном «общий репозиторий».

Теперь посмотрим на картинку. Двапрограммиста, которые мучаются вопросом: создать дублирующий код или же использовать еще раз уже существующий код? Как реализовать первый подход мы уже знаем. Поэтому попробуем разобраться во втором. Поехали.

Паттерн Репозиторий

Этот паттерн предназначен для создания абстракции между слоем доступа к данным и бизнес логикой приложения. Это паттерн доступа к данным, который помогает организовать более слабосвязанный подход. Мы создаем логику доступа к данным в одном классе или их наборе, называемом репозиторий.
В этот раз мы создадим общий репозиторий и класс Unit of work, который будет создавать экземпляр репозитория для каждой сущности. Мы же создадим экземпляр класса UnitOfWork в контроллере, потом создадим экземпляр репозитория, в зависимости от необходимой сущности, ну и в итоги воспользуемся необходимыми методами репозитория.

На следующем изображении показана взаимосвязь между репозиторием и контекстом данных Entity Framework, в котором контроллеры взаимодействуют с репозиторием через Unit of Work, а не непосредственно через Entity Framework.
Теперь давайте разберемся, за что именно отвечает Unit of Work? Он ответственен за создание экземпляра нашего DbContext, в следствие чего, все все репозитории будут использовать один и тот же DbContext для работы с БД. То есть паттерн Unit of Work гарантирует, что все репозитории работают с одним контекстом данных.

Реализация общего репозитория и класса Unit of Work

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

Создаем два проекта: EF.Core и EF.Data. Будем использовать подход «сначала код». EF.Core будет содержать сущности, отображение которых должны быть в БД. В проекте EF.Core создаем две сущности: класс BaseEntity, в котором описываем общие свойства для наследования каждой сущностью и класс Book. Рассмотрим более подробно каждый класс.
Код для BaseEntity выглядит следующим образом

Далее создадим класс Book в папке Data проекта EF.Core, который в свою очередь наследует BaseEntity

В проекте EF.Data содержатся отображение сущности Book, репозиторий и Unit of Work классы. Подход «сначала код» подразумевает создание класса доступа к данным, который наследует от DbContext, поэтому создаем класс EFDbContext, в котором перепишем метод OnModelCreating(). Этот метод вызывается при инициализации модели контекста и позволяет провести дополнительную ее настройку, но перед ее блокировкой. Пример кода этого класса:

Так же следует напомнить, что концепция «сначала код» следует конвенции по конфигурации, поэтому необходимо передать в конструктор строку с названием соединения и которая в точности совпадает с таковой в настройка приложения в файле App.Config. После этих действий возможно автоматическое подключение к серверу БД.
В методе OnModelCreating() используем отражение на карту сущности к классу ее настройки в данном конкретном проекте.

А теперь определим настройки для сущности книги, которые будут использоваться при создании таблицы в БД. Настройки будут находиться в проекте EF.Data, в папке Mapping.

Класс BookMap выглядит так:

Наступил момент создания общего репозитория. Для упрощения примера не будем создавать интерфейс. В репозитории будут реализованы все операции CRUD. Так же в репозитории будет присутствовать параметризированный конструктор, который будет принимать
Context, поэтому при создании экземпляра объекта репозитория все репозитории будут иметь один и тот же контекст. Мы будем использовать метод saveChanges() контекста, однако можно использовать и метод save() класса Unit of Work, так как у них обоих будет один и тот же контекст данных.

Класс общего репозитория выглядит приблизительно так:

Теперь нужно создать класс Unit of Work. Он наследует от интерфейса IDisposable и будет удаляться в каждом контроллере. Так же этот класс будет инициализировать DataContext приложения. А сердцем этого класса будет метод Repository(), который и будет возвращать репозиторий для нужной сущности, которая в свою очередь наследует от BaseEntity. Смотрим:

Приложение MVC использующее паттерн общий репозиторий

Создаем MVC приложение (EF.Web). Это уже будет третий проект в решении, в котором будет содержаться непосредственно пользовательский интерфейс для сущности Book и контроллер для CRUD операций над ней. Создадим контроллер BookController. За каждую CRUD операцию будет отвечать свой ActionResult метод.
В первую очередь создаем экземпляр класса Unit of Work для инициализации конструктором контроллера нужного репозитория для соответствующей сущности.

Теперь нужно создать пользовательский интерфейс.

Create / Edit Book View

Создадим бщий вид для создания и редактирования книги CreateEditBook.cshtml. Для выбора даты публикации книги используем date picker. Код для него на JavaScript:

И непосредственно сам CreateEditBook.cshtml.:

После запуска должны увидеть нечто, похожее на картинку.

Book List View

Эта страница – индекс. Точка входа. Выводим список всех книг.

Ели же нажать на кнопку редактировать то увидим:

Book Detail View

Подробности по выбранной книге.

Delete Book

При нажатии на кнопку удалить попадаем на страничку с общей информацией по книге и кнопкой подтверждения удаления. Используем HttpGet. При нажатии на кнопку подтверждения используем HttpPost.

Источник

Быстрое создание CRUD-основы приложения на Entity Framework/ASP.Net MVC

Предыстория

База данных

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


Остальные таблицы не отличаются чем-либо существенным.

public partial class HomeWorkUnitType : IDataObject
<
public string DisplayName
<
get < return Name; >
>
>

Свойство DisplayName пригодится нам, когда дело дойдет до UI.

Контроллеры

public abstract class DataObjectController : Controller
where T : EntityObject, IDataObject
<
>

private DataModelEntities m_DataContext;
protected DataModelEntities DataContext
<
get
<
if (m_DataContext == null )
<
m_DataContext = new DataModelEntities();
>
return m_DataContext;
>
>

protected abstract IQueryable Table < get ; >
protected abstract Action AddObject

public abstract class DataObjectController : Controller
where T : EntityObject, IDataObject
<
private DataModelEntities m_DataContext;
protected DataModelEntities DataContext
<
get
<
if (m_DataContext == null )
<
m_DataContext = new DataModelEntities();
>
return m_DataContext;
>
>

protected abstract IQueryable Table < get ; >
protected abstract Action AddObject

protected virtual IEnumerable GetAll()
<
foreach ( var t in Table.AsEnumerable())
<
LoadAllDependedObjects(t);
yield return t;
>
>

protected virtual T GetById( int id)
<
var t = Table.First(obj => obj.Id == id);
LoadAllDependedObjects(t);
return t;
>

protected virtual void CreateItem(T data)
<
AddObject(data);
>

protected virtual T EditItem(T data)
<
T existing = GetById(data.Id);
DataContext.ApplyPropertyChanges(existing.EntityKey.EntitySetName, data);
return existing;
>

protected virtual void DeleteItem( int id)
<
DataContext.DeleteObject(GetById(id));
>

public virtual ActionResult Index()
<
return View(GetAll());
>

public virtual ActionResult Details( int id)
<
return View(GetById(id));
>

public virtual ActionResult Create()
<
LoadAllDependedCollections();
return View();
>

[AcceptVerbs(HttpVerbs.Post)]
public virtual ActionResult Create(T data)
<
try
<
ValidateIdOnCreate();
ValidateModel();
CreateItem(data);
DataContext.SaveChanges();
return RedirectToAction( «Index» );
>
catch (Exception ex)
<
LoadAllDependedCollections();
ViewData[ «Error» ] = ex.JoinMessages();
return View(data);
>
>

public virtual ActionResult Edit( int id)
<
LoadAllDependedCollections();
return View(GetById(id));
>

[AcceptVerbs(HttpVerbs.Post)]
public virtual ActionResult Edit(T data)
<
try
<
ValidateModel();
data = EditItem(data);
DataContext.SaveChanges();
return RedirectToAction( «Index» );
>
catch (Exception ex)
<
LoadAllDependedCollections();
ViewData[ «Error» ] = ex.JoinMessages();
return View(data);
>
>

public virtual ActionResult Delete( int id)
<
try
<
ValidateModel();
DeleteItem(id);
DataContext.SaveChanges();
>
catch (Exception ex)
<
ViewData[ «Error» ] = ex.JoinMessages();
>
return RedirectToAction( «Index» );
>

protected void ValidateModel()
<
if (!ModelState.IsValid)
<
throw new Exception( «Model contains errors.» );
>
>

protected virtual void LoadAllDependedCollections()
<
>

protected virtual void LoadAllDependedObjects(T obj)
<
>

protected virtual void ValidateIdOnCreate()
<
ModelState[ «Id» ].Errors.Clear();
>
>

Контроллеры простых объектов

public class HomeWorkUnitTypeController : DataObjectController
<
protected override IQueryable Table
<
get < return DataContext.HomeWorkUnitType; >
>

Контроллеры сложных объектов

protected virtual void LoadAllDependedCollections()
<
>

protected virtual void LoadAllDependedObjects(T obj)
<
>

public class HomeWorkUnitController : DataObjectController
<
protected override IQueryable Table
<
get < return DataContext.HomeWorkUnit; >
>

Источник

Читайте также:  ривердейл в какой озвучке лучше смотреть
Сказочный портал