Использование существующих хранимых процедур для адаптеров таблиц TableAdapter типизированного DataSet (VB)
В предыдущем руководстве мы узнали, как использовать мастер TableAdapter для создания новых хранимых процедур. В этом руководстве мы узнаем, как тот же мастер TableAdapter может работать с существующими хранимыми процедурами. Мы также узнаем, как вручную добавить в базу данных новые хранимые процедуры.
Введение
В предыдущем руководстве мы узнали, как табличные наборы данных в TableAdapters можно настроить для использования хранимых процедур для доступа к данным, а не с помощью нерегламентированных инструкций SQL. В частности, мы изучили, как мастер TableAdapter автоматически создавать эти хранимые процедуры. При переносе устаревшего приложения на ASP.NET 2.0 или при создании веб-сайта ASP.NET 2.0 вокруг существующей модели данных вероятность того, что база данных уже содержит необходимые хранимые процедуры. Кроме того, вы можете создать хранимые процедуры вручную или с помощью какого-либо средства, отличного от мастера TableAdapter, который автоматически создает хранимые процедуры.
В этом руководстве мы рассмотрим, как настроить TableAdapter для использования существующих хранимых процедур. Так как база данных Northwind имеет только небольшой набор встроенных хранимых процедур, мы также рассмотрим шаги, необходимые для ручного добавления новых хранимых процедур в базу данных через среду Visual Studio. Давайте приступим!
Примечание.
В руководстве по изменениям базы данных-оболочке в руководстве по транзакциям мы добавили методы в TableAdapter для поддержки транзакций (BeginTransaction
и CommitTransaction
т. д.). Кроме того, транзакции можно управлять полностью в хранимой процедуре, которая не требует изменений в коде уровня доступа к данным. В этом руководстве мы рассмотрим команды T-SQL, используемые для выполнения инструкций хранимой процедуры в области транзакции.
Шаг 1. Добавление хранимых процедур в базу данных Northwind
Visual Studio упрощает добавление новых хранимых процедур в базу данных. Давайте добавим новую хранимую процедуру в базу данных Northwind, которая возвращает все столбцы из Products
таблицы для тех, у которых есть определенное CategoryID
значение. В окне обозревателя серверов разверните базу данных Northwind, чтобы ее папки — диаграммы баз данных, таблицы, представления и т. д. отображались. Как мы видели в предыдущем руководстве, папка хранимых процедур содержит существующие хранимые процедуры базы данных. Чтобы добавить новую хранимую процедуру, просто щелкните правой кнопкой мыши папку хранимых процедур и выберите параметр "Добавить новую хранимую процедуру" в контекстном меню.
Рис. 1. Щелкните правой кнопкой мыши папку хранимых процедур и добавьте новую хранимую процедуру (щелкните, чтобы просмотреть изображение полного размера)
Как показано на рисунке 1, при выборе параметра "Добавить новую хранимую процедуру" откроется окно скрипта в Visual Studio с структурой скрипта SQL, необходимого для создания хранимой процедуры. Это наша задача, чтобы провести этот скрипт и выполнить его, в какой момент хранимая процедура будет добавлена в базу данных.
Введите следующий скрипт:
CREATE PROCEDURE dbo.Products_SelectByCategoryID
(
@CategoryID int
)
AS
SELECT ProductID, ProductName, SupplierID, CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder,
ReorderLevel, Discontinued
FROM Products
WHERE CategoryID = @CategoryID
Этот скрипт при выполнении добавит новую хранимую процедуру в базу данных Northwind с именем Products_SelectByCategoryID
. Эта хранимая процедура принимает один входной параметр (@CategoryID
тип int
) и возвращает все поля для этих продуктов с соответствующим значением CategoryID
.
Чтобы выполнить этот CREATE PROCEDURE
скрипт и добавить хранимую процедуру в базу данных, щелкните значок "Сохранить" на панели инструментов или нажмите клавиши CTRL+S. После этого папка хранимых процедур обновляется, показывая только что созданную хранимую процедуру. Кроме того, скрипт в окне изменит тонкость на CREATE PROCEDURE dbo.Products_SelectProductByCategoryID
ALTER PROCEDURE
dbo.Products_SelectProductByCategoryID
. CREATE PROCEDURE
добавляет новую хранимую процедуру в базу данных, в то время как ALTER PROCEDURE
обновляет существующую. После начала скрипта ALTER PROCEDURE
изменится, изменив входные параметры хранимых процедур или инструкции SQL и щелкнув значок "Сохранить", изменит хранимую процедуру с этими изменениями.
На рисунке 2 показана Visual Studio после сохранения хранимой Products_SelectByCategoryID
процедуры.
Рис. 2. Хранимая процедура Products_SelectByCategoryID
добавлена в базу данных (щелкните, чтобы просмотреть изображение полного размера)
Шаг 2. Настройка TableAdapter для использования существующей хранимой процедуры
Теперь, когда Products_SelectByCategoryID
хранимая процедура была добавлена в базу данных, мы можем настроить уровень доступа к данным для использования этой хранимой процедуры при вызове одного из его методов. В частности, мы добавим GetProductsByCategoryID(<_i22_>categoryID)<!--_i22_-->
метод ProductsTableAdapter
в NorthwindWithSprocs
набор данных Typed DataSet, который вызывает Products_SelectByCategoryID
только что созданную хранимую процедуру.
Начните с открытия NorthwindWithSprocs
набора данных. Щелкните правой ProductsTableAdapter
кнопкой мыши и выберите команду "Добавить запрос", чтобы запустить мастер настройки запросов TableAdapter. В предыдущем руководстве мы решили создать новую хранимую процедуру для нас. Однако в этом руководстве мы хотим подключить новый метод TableAdapter к существующей Products_SelectByCategoryID
хранимой процедуре. Поэтому выберите вариант "Использовать существующую хранимую процедуру" на первом шаге мастера и нажмите кнопку "Далее".
Рис. 3. Выберите параметр "Использовать существующую хранимую процедуру" (щелкните, чтобы просмотреть изображение полного размера)
На следующем экране представлен раскрывающийся список, заполненный хранимыми процедурами базы данных. Выбор хранимой процедуры содержит входные параметры слева и поля данных, возвращаемые (если таковые) справа. Выберите хранимую Products_SelectByCategoryID
процедуру из списка и нажмите кнопку "Далее".
Рис. 4. Выберите Products_SelectByCategoryID
хранимую процедуру (щелкните, чтобы просмотреть изображение полного размера)
На следующем экране показано, какие данные возвращаются хранимой процедурой, и наш ответ здесь определяет тип, возвращаемый методом TableAdapter. Например, если мы указываем, что табличные данные возвращаются, метод вернет ProductsDataTable
экземпляр, заполненный записями, возвращаемыми хранимой процедурой. В отличие от этого, если указать, что эта хранимая процедура возвращает одно значение, возвращаемое TableAdapter, возвращает Object
значение в первом столбце первой записи, возвращаемой хранимой процедурой.
Так как хранимая Products_SelectByCategoryID
процедура возвращает все продукты, принадлежащие определенной категории, выберите первый ответ — табличные данные — и нажмите кнопку "Далее".
Рис. 5. Указывает, что хранимая процедура возвращает табличные данные (щелкните, чтобы просмотреть изображение полного размера)
Все, что остается, заключается в том, чтобы указать, какие шаблоны методов следует использовать с именами этих методов. Оставьте флажок Fill a DataTable и Return a DataTable, но переименуйте методы FillByCategoryID
в и GetProductsByCategoryID
. Затем нажмите кнопку "Далее", чтобы просмотреть сводку задач, которые будет выполнять мастер. Если все выглядит правильно, нажмите кнопку "Готово".
Рис. 6. Назовите методы FillByCategoryID
и GetProductsByCategoryID
(щелкните, чтобы просмотреть изображение полного размера)
Примечание.
Только что созданные FillByCategoryID
методы TableAdapter и GetProductsByCategoryID
ожидают входного параметра типа Integer
. Это входное значение параметра передается в хранимую процедуру через его @CategoryID
параметр. При изменении Products_SelectByCategory
параметров хранимой процедуры необходимо также обновить параметры для этих методов TableAdapter. Как описано в предыдущем руководстве, это можно сделать одним из двух способов: путем ручного добавления или удаления параметров из коллекции параметров или повторного запуска мастера TableAdapter.
Шаг 3. ДобавлениеGetProductsByCategoryID(categoryID)
метода в BLL
GetProductsByCategoryID
После завершения метода DAL следующим шагом является предоставление доступа к этому методу на уровне бизнес-логики. ProductsBLLWithSprocs
Откройте файл класса и добавьте следующий метод:
<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Select, False)> _
Public Function GetProductsByCategoryID(ByVal categoryID As Integer) _
As NorthwindWithSprocs.ProductsDataTable
Return Adapter.GetProductsByCategoryID(categoryID)
End Function
Этот метод BLL просто возвращает ProductsDataTable
возвращаемый из ProductsTableAdapter
метода S GetProductsByCategoryID
. Атрибут DataObjectMethodAttribute
предоставляет метаданные, используемые мастером настройки источника данных ObjectDataSource. В частности, этот метод появится в раскрывающемся списке вкладок SELECT.
Шаг 4. Отображение продуктов по категориям
Чтобы протестировать только что добавленную Products_SelectByCategoryID
хранимую процедуру и соответствующие методы DAL и BLL, давайте создадим страницу ASP.NET, содержащую DropDownList и GridView. В раскрывающемся списке будут перечислены все категории в базе данных, а GridView будет отображать продукты, принадлежащие выбранной категории.
Примечание.
Мы создали основные и подробные интерфейсы с помощью DropDownLists в предыдущих руководствах. Дополнительные сведения о реализации такого основного или подробного отчета см. в руководстве по фильтрации master/Detail С помощью dropDownList .
ExistingSprocs.aspx
Откройте страницу в AdvancedDAL
папке и перетащите dropDownList из панели элементов в конструктор. Задайте для свойства Categories
DropDownList ID
значение и его AutoPostBack
свойствоTrue
. Затем из смарт-тега привязать DropDownList к новому объекту ObjectDataSource с именем CategoriesDataSource
. Настройте ObjectDataSource, чтобы он извлекал данные из CategoriesBLL
метода класса GetCategories
. Установите раскрывающийся список на вкладках UPDATE, INSERT и DELETE (Нет).
Рис. 7. Получение данных из CategoriesBLL
метода класса GetCategories
(щелкните, чтобы просмотреть изображение полного размера)
Рис. 8. Задайте раскрывающийся список в вкладках UPDATE, INSERT и DELETE (Нет) (Щелкните, чтобы просмотреть изображение полного размера)
После завершения мастера ObjectDataSource настройте DropDownList для отображения CategoryName
поля данных и использования CategoryID
поля в качестве Value
поля для каждого ListItem
.
На этом этапе декларативная разметка DropDownList и ObjectDataSource должны выглядеть следующим образом:
<asp:DropDownList ID="Categories" runat="server" AutoPostBack="True"
DataSourceID="CategoriesDataSource" DataTextField="CategoryName"
DataValueField="CategoryID">
</asp:DropDownList>
<asp:ObjectDataSource ID="CategoriesDataSource" runat="server"
OldValuesParameterFormatString="original_{0}"
SelectMethod="GetCategories" TypeName="CategoriesBLL">
</asp:ObjectDataSource>
Затем перетащите GridView в конструктор, поместив его под dropDownList. Задайте для GridView значение ID
ProductsByCategory
и, из смарт-тега, привязать его к новому объекту ObjectDataSource с именем ProductsByCategoryDataSource
. ProductsByCategoryDataSource
Настройте ObjectDataSource для использования ProductsBLLWithSprocs
класса, получив его данные с помощью GetProductsByCategoryID(categoryID)
метода. Так как этот GridView будет использоваться только для отображения данных, задайте раскрывающийся список на вкладках UPDATE, INSERT и DELETE (Нет) и нажмите кнопку "Далее".
Рис. 9. Настройка ObjectDataSource для использования ProductsBLLWithSprocs
класса (щелкните, чтобы просмотреть изображение полного размера)
Рис. 10. Извлечение данных из GetProductsByCategoryID(categoryID)
метода (щелкните, чтобы просмотреть изображение полного размера)
Метод, выбранный на вкладке SELECT, ожидает параметр, поэтому последний шаг мастера предложит нам источник параметра. Задайте раскрывающийся список источника параметров элементу управления и выберите Categories
элемент управления в раскрывающемся списке ControlID. Чтобы завершить работу мастера, нажмите кнопку Готово .
Рис. 11. Используйте Categories
DropDownList в качестве источника categoryID
параметра (щелкните, чтобы просмотреть изображение полного размера)
После завершения мастера ObjectDataSource Visual Studio добавит BoundFields и CheckBoxField для каждого поля данных продукта. Вы можете настроить эти поля, как вы видите нужные.
Посетите страницу через браузер. При посещении страницы выбрана категория "Напитки" и соответствующие продукты, перечисленные в сетке. Изменение раскрывающегося списка на альтернативную категорию, как показано на рис. 12, вызывает обратную передачу и перезагрузит сетку с продуктами только что выбранной категории.
Рис. 12. Отображаются продукты в категории "Производство" (щелкните, чтобы просмотреть изображение полного размера)
Шаг 5. Упаковка инструкций хранимой процедуры в пределах области транзакции
В руководстве по преобразованию базы данных-оболочки в руководстве по транзакциям мы рассмотрели методы выполнения ряда инструкций изменения базы данных в области транзакции. Помните, что изменения, выполненные под зонтиком транзакции, либо все успешно, либо все неудачно, гарантируя атомарность. Ниже представлены методы использования транзакций:
- Использование классов в
System.Transactions
пространстве имен - Использование уровня доступа к данным используйте классы ADO.NET, например
SqlTransaction
, и - Добавление команд транзакций T-SQL непосредственно в хранимую процедуру
Изменения базы данных-оболочки в руководстве по транзакциям использовали классы ADO.NET в DAL. В оставшейся части этого руководства рассматривается управление транзакцией с помощью команд T-SQL из хранимой процедуры.
Три ключевых команды SQL для запуска, фиксации и отката транзакции выполняются BEGIN TRANSACTION
COMMIT TRANSACTION
вручную и ROLLBACK TRANSACTION
соответственно. Как и в случае с подходом ADO.NET, при использовании транзакций из хранимой процедуры необходимо применить следующий шаблон:
- Укажите начало транзакции.
- Выполните инструкции SQL, составляющие транзакцию.
- Если в любом из операторов из шага 2 возникает ошибка, откат транзакции.
- Если все инструкции из шага 2 завершены без ошибок, зафиксируйте транзакцию.
Этот шаблон можно реализовать в синтаксисе T-SQL с помощью следующего шаблона:
BEGIN TRY
BEGIN TRANSACTION -- Start the transaction
... Perform the SQL statements that makeup the transaction ...
-- If we reach here, success!
COMMIT TRANSACTION
END TRY
BEGIN CATCH
-- Whoops, there was an error
ROLLBACK TRANSACTION
-- Raise an error with the
-- details of the exception
DECLARE @ErrMsg nvarchar(4000),
@ErrSeverity int
SELECT @ErrMsg = ERROR_MESSAGE(),
@ErrSeverity = ERROR_SEVERITY()
RAISERROR(@ErrMsg, @ErrSeverity, 1)
END CATCH
Шаблон начинается с определения TRY...CATCH
блока, создания нового для SQL Server 2005. Как и в Try...Catch
блоках в Visual Basic, блок SQL TRY...CATCH
выполняет инструкции в блоке TRY
. Если любая инструкция вызывает ошибку, элемент управления немедленно передается в CATCH
блок.
Если нет ошибок при выполнении инструкций SQL, которые создают транзакцию, COMMIT TRANSACTION
инструкция фиксирует изменения и завершает транзакцию. Однако если одна из инструкций приводит к ошибке, ROLLBACK TRANSACTION
блок CATCH
возвращает базу данных в состояние до начала транзакции. Хранимая процедура также вызывает ошибку с помощью команды RAISERROR, которая приводит SqlException
к возникновению в приложении.
Примечание.
Так как блок не является новым для SQL Server 2005, приведенный TRY...CATCH
выше шаблон не будет работать, если вы используете более старые версии Microsoft SQL Server.
Рассмотрим конкретный пример. Ограничение внешнего ключа существует между Categories
таблицами и Products
таблицами, что означает, что каждое CategoryID
поле в Products
таблице должно сопоставляться со CategoryID
значением Categories
в таблице. Любое действие, которое нарушает это ограничение, например попытку удалить категорию, связанную с продуктами, приводит к нарушению ограничений внешнего ключа. Чтобы проверить это, вернитесь к примеру "Обновление и удаление существующих двоичных данных" в разделе "Работа с двоичными данными" (~/BinaryData/UpdatingAndDeleting.aspx
). На этой странице перечислены все категории в системе вместе с кнопками "Изменить" и "Удалить" (см. рис. 13), но при попытке удалить категорию, связанную с продуктами , такими как напитки, удаление завершается ошибкой из-за нарушения ограничений внешнего ключа (см. рис. 14).
Рис. 13. Каждая категория отображается в GridView с кнопками редактирования и удаления (щелкните, чтобы просмотреть изображение полного размера)
Рис. 14. Удалить категорию с существующими продуктами (щелкните, чтобы просмотреть изображение полного размера)
Представьте себе, что мы хотим разрешить удалять категории независимо от того, имеют ли они связанные продукты. Если категория с продуктами будет удалена, представьте, что мы хотим также удалить существующие продукты (хотя другим вариантом будет просто задать значения NULL
продуктовCategoryID
). Эту функцию можно реализовать с помощью каскадных правил ограничения внешнего ключа. Кроме того, можно создать хранимую процедуру, которая принимает @CategoryID
входной параметр, и при вызове явным образом удаляет все связанные продукты, а затем указанную категорию.
Первая попытка такой хранимой процедуры может выглядеть следующим образом:
CREATE PROCEDURE dbo.Categories_Delete
(
@CategoryID int
)
AS
-- First, delete the associated products...
DELETE FROM Products
WHERE CategoryID = @CategoryID
-- Now delete the category
DELETE FROM Categories
WHERE CategoryID = @CategoryID
Хотя это, безусловно, приведет к удалению связанных продуктов и категорий, это не делается под зонтиком транзакции. Представьте, что существует другое ограничение внешнего ключа, Categories
которое запрещает удаление определенного @CategoryID
значения. Проблема заключается в том, что в таком случае все продукты будут удалены перед попыткой удалить категорию. Чистый результат заключается в том, что для такой категории эта хранимая процедура будет удалять все его продукты в то время как категория остается, так как она по-прежнему имеет связанные записи в какой-то другой таблице.
Если хранимая процедура была заключена в область транзакции, однако удаление Products
таблицы будет откатировано в лицо сбоем удаления Categories
. Следующий скрипт хранимой процедуры использует транзакцию для обеспечения атомарности между двумя DELETE
операторами:
CREATE PROCEDURE dbo.Categories_Delete
(
@CategoryID int
)
AS
BEGIN TRY
BEGIN TRANSACTION -- Start the transaction
-- First, delete the associated products...
DELETE FROM Products
WHERE CategoryID = @CategoryID
-- Now delete the category
DELETE FROM Categories
WHERE CategoryID = @CategoryID
-- If we reach here, success!
COMMIT TRANSACTION
END TRY
BEGIN CATCH
-- Whoops, there was an error
ROLLBACK TRANSACTION
-- Raise an error with the
-- details of the exception
DECLARE @ErrMsg nvarchar(4000),
@ErrSeverity int
SELECT @ErrMsg = ERROR_MESSAGE(),
@ErrSeverity = ERROR_SEVERITY()
RAISERROR(@ErrMsg, @ErrSeverity, 1)
END CATCH
На некоторое время добавьте Categories_Delete
хранимую процедуру в базу данных Northwind. Вернитесь к шагу 1, чтобы получить инструкции по добавлению хранимых процедур в базу данных.
Шаг 6. ОбновлениеCategoriesTableAdapter
Хотя мы добавили Categories_Delete
хранимую процедуру в базу данных, DAL в настоящее время настраивается для использования нерегламентированных инструкций SQL для выполнения удаления. Нам нужно обновить CategoriesTableAdapter
и указать ему использовать Categories_Delete
хранимую процедуру.
Примечание.
Ранее в этом руководстве мы работали с набором NorthwindWithSprocs
данных. Но этот набор данных имеет только одну сущность, ProductsDataTable
и нам нужно работать с категориями. Таким образом, для остальной части этого руководства, когда я говорю о уровне доступа к данным, который я ссылаюсь на Northwind
Набор данных, который мы впервые создали в руководстве по созданию уровня доступа к данным.
Откройте набор данных Northwind, выберите CategoriesTableAdapter
и перейдите к окно свойств. Окно свойств перечисляет InsertCommand
,UpdateCommand
DeleteCommand
, и используется TableAdapter, а также его имя и SelectCommand
сведения о подключении. DeleteCommand
Разверните свойство, чтобы просмотреть его сведения. Как показано на рисунке 15, DeleteCommand
свойство s CommandType
имеет значение Text, которое указывает ему отправить текст в свойстве в CommandText
виде нерегламентированного SQL-запроса.
Рис. 15. Выберите CategoriesTableAdapter
конструктор, чтобы просмотреть его свойства в окне свойств
Чтобы изменить эти параметры, выберите текст (DeleteCommand) в окно свойств и выберите (Создать) из раскрывающегося списка. Это приведет к очистке параметров для CommandText
свойств CommandType
, а также Parameters
свойств. Затем задайте CommandType
для свойства StoredProcedure
значение и введите имя хранимой процедуры для CommandText
(dbo.Categories_Delete
). Если вы обязательно введите свойства в этом порядке — сначала CommandType
, а затем CommandText
— Visual Studio автоматически заполняет коллекцию параметров. Если вы не вводите эти свойства в этом порядке, необходимо вручную добавить параметры через редактор коллекции параметров. В любом случае необходимо щелкнуть многоточие в свойстве "Параметры", чтобы открыть редактор коллекции параметров, чтобы убедиться, что были внесены правильные параметры параметров (см. рис. 16). Если в диалоговом окне нет параметров, добавьте @CategoryID
этот параметр вручную (не нужно добавлять @RETURN_VALUE
этот параметр).
Рис. 16. Убедитесь в правильности параметров
После обновления DAL удаление категории автоматически удаляет все связанные с ним продукты и делает это под зонтиком транзакции. Чтобы проверить это, вернитесь на страницу "Обновление и удаление существующих двоичных данных" и нажмите кнопку "Удалить" для одной из категорий. При одном щелчке мыши категория и все связанные с ней продукты будут удалены.
Примечание.
Перед тестированием Categories_Delete
хранимой процедуры, которая будет удалять ряд продуктов вместе с выбранной категорией, может быть разумно сделать резервную копию базы данных. Если вы используете NORTHWND.MDF
базу данных, App_Data
просто закройте Visual Studio и скопируйте файлы MDF и LDF в App_Data
другую папку. После тестирования функциональных возможностей можно восстановить базу данных, закрыв Visual Studio и заменив текущие файлы MDF и LDF на App_Data
копии резервных копий.
Итоги
Хотя мастер TableAdapter автоматически создает хранимые процедуры для нас, иногда возникают случаи, когда у нас уже есть такие хранимые процедуры или хотите создать их вручную или с другими инструментами. Для реализации таких сценариев можно также настроить TableAdapter для указания существующей хранимой процедуры. В этом руководстве мы рассмотрели, как вручную добавлять хранимые процедуры в базу данных через среду Visual Studio и как подключить методы TableAdapter к этим хранимым процедурам. Мы также изучили команды T-SQL и шаблон скрипта, используемые для запуска, фиксации и отката транзакций из хранимой процедуры.
Счастливое программирование!
Об авторе
Скотт Митчелл, автор семи книг ASP/ASP.NET и основатель 4GuysFromRolla.com, работает с технологиями Microsoft Web с 1998 года. Скотт работает независимым консультантом, тренером и писателем. Его последняя книга Сэмс Учит себя ASP.NET 2.0 в 24 часах. Он может быть достигнут в mitchell@4GuysFromRolla.com. или через его блог, который можно найти на http://ScottOnWriting.NET.
Особое спасибо
Эта серия учебников была проверена многими полезными рецензентами. Ведущие рецензенты для этого руководства были Хилтон Geisenow, S ren Джейкоб Лоритсен и Тереса Мерфи. Хотите просмотреть мои предстоящие статьи MSDN? Если да, упадите меня линию в mitchell@4GuysFromRolla.com.
Обратная связь
https://aka.ms/ContentUserFeedback.
Ожидается в ближайшее время: в течение 2024 года мы постепенно откажемся от GitHub Issues как механизма обратной связи для контента и заменим его новой системой обратной связи. Дополнительные сведения см. в разделеОтправить и просмотреть отзыв по