Назад | Содержание | Вперед |
Многозвенные распределенные приложения обеспечивают эффективный доступ удаленных клиентов к базе данных, так как в них для управления доступом к данным применяется специализированное ПО промежуточного слоя. В наиболее распространенной схеме — трехзвенном приложении — это сервер приложения, который выполняет следующие функции:
Delphi обеспечивает разработку серверов приложений на основе использования ряда технологий:
В этой главе рассматриваются следующие вопросы:
Итак, сервер приложения — это ПО промежуточного слоя трехзвенного распределенного приложения (см. рис. 20.2). Его основой является удаленный модуль данных. В Delphi предусмотрено использование удаленных модулей данных пяти типов (см. ниже).
Далее в этой главе мы детально рассмотрим вопросы использования удаленных модулей данных, инкапсулирующих функции серверов Автоматизации. Другие типы удаленных модулей данных рассматриваются в следующих частях книги.
Каждый удаленный модуль данных инкапсулирует интерфейс IAppServer, методы которого используются в механизме удаленного доступа клиентов к серверу БД (см. гл. 20).
Для обмена данными с сервером БД модуль данных может содержать некоторое количество компонентов доступа к данным (компонентов соединений и компонентов, инкапсулирующих набор данных).
Для обеспечения передачи данных клиентам удаленный модуль данных обязательно должен содержать необходимое количество компонентов TDataSetProvider, каждый из которых должен быть связан с соответствующим набором данных.
Внимание
Обмен данными сервера приложения с клиентами обеспечивает динамическая библиотека MIDAS.DLL, которая должна быть зарегистрирована на компьютере сервера приложения.
Для создания нового сервера приложения достаточно выполнить несколько простых операций.
1. Создать новый проект, выбрав в качестве типа проекта обычное приложение (пункт меню File | New | Application) и сохранить его.
2. В зависимости от используемой технологии, выбрать из Репозитория Delphi необходимый тип удаленного модуля данных (см. рис. 20.3). Удаленные модули данных располагаются на страницах Multitier, WebSnap и WebServices.
3. Настроить параметры создаваемого удаленного модуля данных (см. ниже).
4. Разместить в удаленном модуле данных компоненты доступа к данным и настроить их. Здесь разработчик может выбрать один из имеющихся
наборов компонентов (см. часть IV) в зависимости от используемого сервера БД и требуемых характеристик создаваемого приложения.
5. Разместить в удаленном модуле данных необходимое число компонентов TDataSetProvider и связать их с компонентами, инкапсулирующими наборы данных.
6. При необходимости создать для потомка интерфейса IAppServer, используемого в удаленном модуле данных, дополнительные методы. Для этого создается новая библиотека типов (см. низке).
7. Скомпилировать проект и создать исполняемый файл сервера приложения.
8. Зарегистрировать сервер приложения и при необходимости настроить дополнительное ПО.
Весь механизм удаленного доступа, инкапсулированный в удаленных модулях данных и компонентах-провайдерах, работает автоматически, без создания разработчиком дополнительного программного кода.
Далее в этой главе на простом примере рассматриваются все перечисленные этапы создания сервера приложения.
Интерфейс IAppServer является основной механизма удаленного доступа клиентских приложений к серверу приложения. Набор данных клиента использует его для общения с компонентом-провайдером на сервере приложения. Наборы данных клиента получают экземпляр IAppServer от компонента соединения в клиентском приложении (см. рис. 20.2).
При создании удаленных модулей данных (см. ниже) каждому такому модулю ставится в соответствие вновь создаваемый интерфейс, предком которого является интерфейс IAppServer.
Разработчик может добавить к новому интерфейсу собственные методы, которые, благодаря возможностям механизма удаленного доступа многозвенных приложений, становятся доступны приложению-клиенту.
Свойство
property AppServer: Variant;
в клиентском приложении имеется как в компонентах удаленного соединения, так и клиентском наборе данных.
По умолчанию интерфейс является несохраняющим состояние (stateless). Это означает, что вызовы методов интерфейса независимы и не привязаны
к предыдущему вызову. Поэтому интерфейс IAppServer не имеет свойств, которые бы хранили информацию о состоянии между вызовами.
Обычно разработчику ни к чему использовать методы интерфейса напрямую, однако его значение для многозвенных приложений трудно переоценить. И при детальной работе с механизмом удаленного доступа интерфейс понадобится так или иначе.
Методы интерфейса IAppServer представлены в табл. 21.1
Таблица 21.1. Методы интерфейса IAppServar
Объявление |
Описание |
function AS ApplyUpdates (const ProviderName: WideString; Delta: OleVariant; MaxErrors: Integer; out ErrorCount: Integer; var OwnerData: OleVariant) : OleVariant; safecall; |
Передает изменения, полученные от клиентского набора данных, компоненту-провайдеру, определяемому параметром ProviderName. Изменения содержатся в параметре Delta. Параметр MaxErrors задает максимальное число ошибок, пропускаемых при сохранении данных перед прерыванием операции. Реальное число возникших ошибок возвращается параметром ErrorCount. Параметр OwnerData содержит дополнительную информацию, передаваемую между клиентом и сервером (например, значения параметров методов-обработчиков). Функция возвращает пакет данных, содержащий все записи, которые не были сохранены в базе данных по какой-либо причине |
function AS DataRequest (const ProviderName: WideString; Data: OleVariant) : OleVariant; safecall; |
Генерирует событие OnDataRequest для указанного провайдера ProviderName |
procedure AS Execute (const ProviderName: WideString; const CommandText : WideString; var Params : OleVariant; var OwnerData: OleVariant) ; safecall; |
Выполняет запрос или хранимую процедуру, определяемые параметром CommandText для провайдера, указанного параметром ProviderName. Параметры запроса или хранимой процедуры содержатся в параметре Params |
function AS GetParams (const ProviderName: WideString; var OwnerData: OleVariant): OleVariant; safecall; |
Передает провайдеру ProviderName текущие значения параметров клиентского набора данных |
function AS GetProviderNames: OleVariant; safecall; |
Возвращает список всех доступных провайдеров удаленного модуля данных |
function AS GetRecords (const ProviderName : WideString, Count: Integer; out RecsOut: Integer; Options: Integer; const CommandText: WideString; var Params : OleVariant; var OwnerData :OieVariant) : OleVariant; safecall; |
Возвращает пакет данных с записями набора данных сервера, связанного с компонентом-провайдером. Параметр CommandText содержит имя таблицы, текст запроса или имя хранимой процедуры, откуда необходимо получить записи. Но он работает только в случае, если для провайдера в параметре Options включена опция poAllowCommandText. Параметры запроса или процедуры помещаются в параметре Params. Параметр задает требуемое число записей, начиная с текущей, если его значение больше нуля. Если параметр равен нулю — возвращаются только метаданные, если он равен -1 — возвращаются все записи. Параметр RecsOut возвращает реальное число переданных записей |
function AS RowRequest (const ProviderName: WideString; Row: OleVariant; RequestType: Integer; var OwnerData: OleVariant): OleVariant; safecall; |
Возвращает запись набора данных (предоставляемого провайдером ProviderName), определяемую параметром Row. Параметр RequestType содержит значение типа TfetchOptions |
Большинство методов интерфейса используют параметры ProviderName и OwnerData. Первый определяет имя компонента-провайдера, а второй содержит набор параметров, передаваемых для использования в методах-обработчиках.
Внимательный читатель обратил внимание, что использование метода AS_GetRecords подразумевает сохранение информации при работе интерфейса, т. к. метод возвращает записи, начиная с текущей, хотя интерфейс IAppServer имеет тип stateless. Поэтому перед использованием метода рекомендуется обновлять набор данных клиента.
Тип
TFetchOption = (foRecord, foBlobs, foDetails);
TFetchOptions = set of TFetchOption;
используется в параметре RequestType метода AS_RowRequest.
foRecord — возвращает значения полей текущей записи;
foBlobs — возвращает значения полей типа BLOB текущей записи;
foDetails — возвращает все подчиненные записи вложенных наборов данных для текущей записи.
Для организации взаимодействия клиентов с сервером БД удаленный модуль данных сервера приложения должен содержать компоненты-провайдеры TDataSetProvider (см. гл. 20). При этом используются методы интерфейса IAppServer.
Для обмена данными с набором данных на сервере компонент-провайдер применяет интерфейс IProviderSupport (см. рис. 20.2), который включен в любой компонент набора данных, произошедший от класса TDataSet. В зависимости от используемой технологии доступа к данным каждый компонент, инкапсулирующий набор данных, имеет собственную реализацию методов интерфейса IProviderSupport.
Методы интерфейса могут понадобится разработчику только при создании собственных компонентов, инкапсулирующих набор данных и наследующих от класса TDataSet.
Удаленный модуль данных является основой сервера приложения (см. рис. 20.2) для многозвенного распределенного приложения. Во-первых, он выполняет функции обычного модуля данных — на нем можно размещать компоненты доступа к данным. Во-вторых, удаленный модуль данных инкапсулирует интерфейс IAppServer, обеспечивая тем самым выполнение функций сервера и обмен данными с удаленными клиентами.
В зависимости от используемой технологии в Delphi можно использовать удаленные модули данных пяти типов.
Ниже мы рассмотрим процесс создания сервера приложения на основе удаленного модуля данных TRemoteDataModule. Остальные модули данных (за исключением удаленного модуля данных для CORBA) детально рассматриваются далее в этой книге.
Для создания удаленного модуля данных TRemoteDataModule используется Репозиторий Delphi (команда File | New | Other). Значок класса TRemoteDataModuie находится на странице Multitier (см. рис. 20.3). Перед созданием экземпляра удаленного модуля данных появляется диалоговое окно (рис. 21.1), в котором необходимо предустановить три параметра.
Рис 21.1. Мастер создания удаленного модуля данных TRemoteDataModule
Строка CoClass Name должна содержать имя нового модуля данных, которое будет также использовано для именования нового класса, создаваемого для поддержки нового модуля данных.
Список Instancing позволяет задать способ создания модуля данных.
Список Threading Model задает механизм обработки запросов клиентов.
При создании нового удаленного модуля данных создается специальный класс — наследник класса TRemoteDataModule. И фабрика класса на основе класса TComponentFactory
Примечание
Класс TComponentFactory представляет собой фабрику класса для компонентов Delphi, инкапсулирующих интерфейсы. Поддерживает интерфейс IClass Factory.
Создадим, например, удаленный модуль данных simpleRDM. В мастере создания модуля данных в качестве способа создания выберем Single Instance, a Free — как модель обработки запросов.
Листинг 21.1. Исходный код нового удаленного модуля данных и его фабрики класса
type
TSimpleRDM = class(TRemoteDataModuie, ISimpleRDM)
private
( Private declarations }
protected
class procedure UpdateRegistry(Register: Boolean;
const Classic,
ProgID: string);
override;
public
{ Public declarations }
end;
implementation
{$R *.DFM}
class procedure TSimpleRDM.UpdateRegistry(Register: Boolean;
const
ClassID, ProgID: string);
begin
if Register then
begin
inherited UpdateRegistry(Register, Classic, ProgID);
EnableSocketTransport(ClassID);
EnableWebTransport(ClassID);
end
else
begin
DisableSocketTransport(ClassID);
DisableWebTransport(ClassID);
inherited UpdateRegistry(Register, ClassID, ProgID);
end;
end;
initialization
TComponentFactory.Create(ComServer, TSimpleRDM,
Class_SimpleRDM, ciMultilnstance, tmApartment);
end.
Обратите внимание, что параметры модуля данных, заданные при создании, использованы в фабрике класса TComponentFactory в секции initialization.
Примечание
Фабрика класса TComponentFactory обеспечивает создание экземпляров компонентов Delphi, поддерживающих использование интерфейсов.
Метод класса UpdateRegistry создается автоматически и обеспечивает регистрацию и аннулирование регистрации сервера Автоматизации. Если параметр Register имеет значение True, выполняется регистрация, иначе — отмена регистрации.
Разработчик не должен использовать этот метод, т. к. его вызов осуществляется автоматически.
Одновременно с модулем данных создается и его интерфейс — потомок интерфейса IAppServer. Его исходный код содержится в библиотеке типов проекта сервера приложения. Для удаленного модуля данных simpleRDM созданный интерфейс isimpleRDM представлен в листинге 21.2. Для удобства из листинга удалены автоматически добавляемые комментарии.
Листинг 21.2. Вновь созданная библиотека типов для сервера приложения с исходным кодом интерфейса удаленного модуля данных
LIBID_SimpleAppSrvr: TGUID='(93577575-OF4F-43B5-9FBE-A5745128D9A4}';
IID_ISimpleRDM: TGUID = '{Е2СВЕВСВ-1950-4054-В823-62906306Е840}'; CLASS_SimpleRDM: TGUID = '{DB6A6463-5F61-485F-8F23-EC6622091908}' ;
type
ISimpleRDM = interface;
ISimpleRDMDisp = dispinterface;
SimpleRDM = ISimpleRDM;
ISimpleRDM = interface(lAppServer)
['{E2CBEBCB-1950-4054-B823-62906306E840}']
end;
ISimpleRDMDisp = dispinterface
['{E2CBEBCB-1950-4054-B823-62906306E840}']
function AS_ApplyUpdates(const ProviderName: WideString; Delta: OleVariant; MaxErrors: Integer; out ErrorCount: Integer; var OwnerData: OleVariant): OleVariant; dispid 20000000; function AS_GetRecords(const ProviderName: WideString; Count: Integer; out RecsOut: Integer; Options: Integer; const CommandText: WideString; var Params: OleVariant; var OwnerData: OleVariant): OleVariant; dispid 20000001;
function AS_DataRequest(const ProviderName: WideString; Data: OleVariant): OleVariant; dispid 20000002;
function AS_GetProviderNames: OleVariant; dispid 20000003;
function AS_GetParams(const ProviderName: WideString;
var OwnerData: OleVariant): OleVariant; dispid 20000004;
function AS_RowRequest(const ProviderName: WideString; Row: OleVariant; RequestType: Integer; var OwnerData: OleVariant): OleVariant; dispid 20000005;
procedure AS_Execute(const ProviderName: WideString; const CommandText: WideString; var Params: OleVariant; var OwnerData: OleVariant);
dispid 20000006;
end;
CoSimpleRDM = class
class function Create: ISimpleRDM;
class function CreateRemote(const MachineName: string): ISimpleRDM;
end;
imp1ementation uses ComObj;
class function CoSimpleRDM.Create: ISimpleRDM;
begin
Result := CreateComObject(CLASS_SimpleRDM) as ISimpleRDM;
end;
class function CoSimpleRDM.CreateRemote(const MachineName: string): ISimpleRDM;
begin
Result := CreateRemoteComObject(MachineName, CLASS_SimpleRDM)
as ISimpleRDM;
end;
end.
Обратите внимание, что интерфейс ISimpleRDM является потомком интерфейса IAppServer, рассмотренного выше.
Так как удаленный модуль данных реализует сервер Автоматизации, дополнительно к основному дуальному интерфейсу ISimpleRDM автоматически создан интерфейс диспетчеризации isimpleRDMDisp. При этом для интерфейса диспетчеризации созданы методы, соответствующие методам интерфейса IAppServer.
Класс coSimpleRDM обеспечивает создание СОМ-объектов, поддерживающих использование интерфейса. Для него автоматически созданы два метода класса.
Метод
class function Create: ISimpleRDM;
используется при работе с локальным и внутренним сервером (in process).
Метод
class function CreateRemote(const MachineName: string): ISimpleRDM;
используется в удаленном сервере.
Оба метода возвращают ссылку на интерфейс ISimpleRDM.
Теперь, если проект с созданным модулем данных сохранить и зарегистрировать, он станет доступен в удаленных клиентских приложениях как сервер приложения.
После создания удаленный модуль данных становится платформой для размещения компонентов доступа к данным и компонентов провайдеров (см. гл. 20), которые, наряду с модулем данных, реализуют основные функции сервера приложения.
Один сервер приложения может содержать несколько удаленных модулей данных, которые, например, выполняют различные функции или обращаются к разным серверам БД. В этом случае процесс разработки серверной части не претерпевает изменений. При выборе имени сервера в компоненте удаленного соединения на стороне клиента (см. гл. 22) будут доступны имена всех удаленных модулей данных, включенных в состав сервера приложения.
Однако тогда для каждого модуля понадобится собственный компонент соединения. Если это нежелательно, можно использовать компонент TSharedConnection, но в этом случае в интерфейсы удаленных модулей данных необходимо внести изменения.
Для того чтобы несколько модулей данных были доступны в рамках одного удаленного соединения, необходимо выделить один главный модуль данных, а остальные сделать дочерними.
Рассмотрим, что же это означает для практики создания удаленных модулей данных. Суть идеи проста. Интерфейс главного модуля данных (разработчик назначает модуль главным, исходя из собственных соображений) должен содержать свойства, указывающие на интерфейсы всех других модулей данных, которые также необходимо использовать в рамках одного соединения на клиенте. Такие модули данных и называются дочерними.
Если такие свойства (свойство должно иметь атрибут только для чтения) существуют, все дочерние модули данных будут доступны в свойстве ChildName Компонента TSharedConnection (см. гл. 20).
Например, если дочерний удаленный модуль данных носит название Secondary, главный модуль данных должен содержать свойство Secondary:
ISimpleRDM = interface(lAppServer)
['{E2CBEBCB-1950-4054-B823-62906306E840}'] function Get_Secondary: Secondary; safecall;
property Secondary: Secondary read Get_Secondary;
end;
Реализация метода Get_secondary выглядит так:
function TSimpleRDM.Get_Secondary: Secondary;
begin
Result := FSecondaryFactory.CreateCOMObject(nil) as ISecondary;
end;
Как видите, в простейшем случае достаточно вернуть ссылку на вновь созданный дочерний интерфейс.
Полностью пример создания дочернего удаленного модуля данных рассматривается далее в этой главе.
Для того чтобы клиент мог "увидеть" сервер приложения, он должен быть зарегистрирован на компьютере сервера. В зависимости от используемой технологии процесс регистрации имеет особенности. Регистрация серверов MTS, Web и SOAP рассматривается далее в этой книге.
Здесь же мы остановимся на регистрации сервера приложения, использующего удаленный модуль данных TRemoteDataModule (сервер Автоматизации), который чрезвычайно прост.
Для исполняемых файлов достаточно запустить сервер с ключом /regserver или даже просто запустить исполняемый файл.
В среде разработки ключ можно поместить в диалоге команды меню Run Parameters (рис. 21.2).
Рис. 21.2. Диалог параметров запуска приложения
Для удаления регистрации используется ключ /unregserver, но только в командной строке.
Для регистрации динамических библиотек применяется ключ /regsvr32.
В качестве примера рассмотрим процесс создания простого сервера приложения на основе удаленного модуля данных TRemoteDataModule. Для начала создадим новый проект — простое исполняемое приложение и сохраним его под именем simpleAppSrvr (табл. 21.2). Этот проект входит в состав группы проектов simpleRemote, в нее впоследствии будет добавлено клиентское приложение.
Таблица 21.2. Файлы проекта simpieAppSrvr
Файл |
Назначение |
uSimpleAppSrvr.pas |
Стандартный файл проекта |
SimpleAppSrvr_TLB.pas |
Библиотека типов. Содержит объявления всех используемых в проекте интерфейсов |
uSimpleRDM.pas |
Файл главного удаленного модуля данных SimpleRDM |
uSecondary.pas |
Файл дочернего удаленного модуля данных Secondary |
Пример создания клиента для сервера приложения simpieAppSrvr рассматривается в гл. 22.
Добавим в проект новый удаленный модуль данных, используя для этого Репозиторий Delphi (см. рис. 20.3). Затем в появившемся диалоге (см. рис. 21.1) зададим имя модуля — simpleRDM и его параметры:
Метод класса updateRegistry для модуля данных создается автоматически и обеспечивает регистрацию и аннулирование регистрации сервера Автоматизации (см. листинг 21.1).
Одновременно с удаленным модулем данных автоматически создается библиотека типов и в ней дуальный интерфейс isimpleRDM и интерфейс диспетчеризации ISimpleRDMDisp (см. Листинг 21.2).
Примечание
Для каждого вновь созданного интерфейса автоматически назначается GUID.
Разместим в модуле simpleRDM компоненты для доступа к файлам демонстрационной базы данных (\Program Files\Common Files\Borland Shared\Data) через драйвер BDE и псевдоним DBDEMOS, который создается автоматически при инсталляции Delphi. Это компонент TDatabase, обеспечивающий соединение и три табличных компонента TTаblе, инкапсулирующих наборы данных из таблиц Orders.db, Customer.db, Employee.db.
Компонент соединения настроен на псевдоним DBDEMOS (свойство AliasName). В параметрах соединения заданы имя пользователя и пароль, а свойство LoginPrompt = False запрещает отображение диалога регистрации при открытии соединения. Каждый табличный компонент связан с компонентом-провайдером TDataSetProvider. Свойство провайдера ResolveToDataSet = False запрещает передачу изменений, полученных от клиента, в набор данных связанного компонента. Вместо этого данные напрямую сохраняются в базе данных. Это увеличивает быстродействие приложения.
Дополнительно к основному модулю данных создадим дочерний модуль данных secondary. Для того чтобы связать главный модуль данных с дочерним, необходимо добавить к интерфейсу isimpleRDM метод, возвращающий ссылку на интерфейс дочернего модуля данных. В нашем примере это метод
Get_Secondary.
Для его создания воспользуемся библиотекой типов сервера (рис. 21.3).
Рис. 21.3. Библиотека типов сервера приложения SimpleAppSrvr
В дереве в левой части окна выберем интерфейс isimpleRDM и создадим для него новое свойство только для чтения, переименуем его в secondary. Одновременно со свойством будет создан метод, обеспечивающий чтение свойства. Переименуем его в Get_secondary. Метод должен возвращать тип secondary. Для его установки воспользуемся списком Туре на странице Attributes в правой части панели окна библиотеки типов (см. рис. 21.3).
После обновления исходного кода библиотеки типов (кнопка Refresh Implementation) описание нового свойства и метода интерфейса isimpleRDM появится в файле SimpleAppSrvr_TLB.pas. Теперь объявление интерфейса isimpieRDM выглядит так:
ISimpleRDM = interface(lAppServer)
['{Е2СВЕВСВ-1950-4054-В823-62906306Е840}']
function Get_Secondary: Secondary; safecall;
property Secondary: Secondary read Get_Secondary;
end;
Одновременно в объявлении удаленного модуля данных simpleRDM в файле uSimpleRDM появится метод Get_secondary. Его исходный код должен выглядеть следующим образом:
function TSimpleRDM.Get_Secondary: Secondary;
begin
Result := FSecondaryFactory.CreateCOMObject(nil) as ISecondary;
end;
Теперь модуль данных secondary стал дочерним для модуля simpleRDM.
Модуль secondary содержит компоненты для доступа к локальному серверу InterBase. База данных mastsql.gdb, используемая в этом примере, поставляется вместе с Delphi. Соединение обеспечивается компонентом TiBDatabase, который настроен на базу данных при помощи свойства DatabaseName.
Перед компиляцией проекта необходимо правильно настроить свойство DatabaseName, если местоположение файла mastsql.gdb отличается от обычного.
Два табличных компонента TTBTаblе инкапсулируют таблицы Vendors и Parts из базы данных mastsql.gdb. Дополнительно между этими двумя компонентами установлено отношение "один-ко-многим". Свойство MasterSource компонента tbiParts указывает на компонент dsvendors (класс TDataSource), связанный с компонентом tblVendors.Свойства MasterFields и indexFieidNames компонента tbiParts содержат имя общего для двух таблиц поля vendorNo (подробнее о создании отношения "один-ко-многим" см. гл. 14).
Отношение "один-ко-многим", созданное для двух таблиц, позволит продемонстрировать в примере клиентского приложения использование вложенных наборов данных (см. гл. 22).
Теперь, когда сервер приложения готов, остался последний этап — регистрация сервера. Для этого достаточно запустить исполняемый файл проекта на компьютере, который будет использоваться для работы сервера приложения. Но обратите внимание, что в этом случае настройки доступа к используемым в примере базам данных должны быть скорректированы с учетом переноса на другой компьютер. И естественно, на нем должны быть установлен BDE и клиент InterBase.
После регистрации сервер приложения доступен из всех клиентских приложений, компоненты соединения DataSnap настроены на компьютер сервера приложения.
Сервер приложения представляет собой ПО промежуточного слоя для трехзвенных распределенных приложений. Он обеспечивает связь удаленных клиентов с сервером БД и реализует большую часть бизнес-логики распределенного приложения.
В Delphi сервер приложения создается на основе удаленных модулей данных, реализация которых различается для различных технологий удаленного доступа. Удаленные модули данных имплементируют интерфейс IAppServer. Непосредственный доступ к данным обеспечивают компоненты-провайдеры TDataSetProvider при помощи интерфейса IProviderSupport.
Назад | Содержание | Вперед |