Перейти к содержимому

Миграция Delphi 6 → 12/13

Как перевести большой клиент с Delphi 6 на Delphi 12/13: что ломает Unicode, чем FireDAC отличается от ADO, что меняется в 64 битах, как спланировать миграцию без остановки бизнеса и как постепенно переносить функции в .NET-сервисы. На собеседовании здесь проверяют, знаете ли вы, где миграция тихо портит данные и как поймать это раньше пользователей.

Перед этой темой: Object Pascal, VCL и доступ к данным.

Middle+

Что сломалось при переходе на Unicode в Delphi 2009 и как это чинить?

Заголовок раздела «Что сломалось при переходе на Unicode в Delphi 2009 и как это чинить?»
Ответ

Что это

В Delphi 6 string — это AnsiString: один байт на символ, текст в кодовой странице Windows, для кириллицы обычно 1251. С Delphi 2009 string — это UnicodeString: два байта на символ, кодировка UTF-16.

Что поменялось

Тип Delphi 6 Delphi 2009+
string AnsiString UnicodeString
Char AnsiChar, 1 байт WideChar, 2 байта
PChar PAnsiChar PWideChar

Как пользоваться

  • Обычная работа со строками — сложение, Copy, Pos, Length — не меняется.
  • Ломается код, где строка — это байты: запись в поток, буферы для WinAPI, вызовы старых DLL.

Частая ошибка

Считать размер буфера символов через SizeOf. Он возвращает байты, а Length — число символов, и в Unicode это разные числа.

Как устроено

  • AnsiString теперь знает свою кодовую страницу: type Cp1251String = type AnsiString(1251). При присваивании между AnsiString и string компилятор вставляет преобразование.
  • Функции WinAPI без суффикса (GetWindowText) теперь указывают на W-версии. Они ждут PWideChar, а размер буфера — в символах.

Было — стало — что делать

Было в Delphi 6 Что ломается Что делать
Stream.Write(S[1], Length(S)) пишется половина строки байты из TEncoding, TStreamWriter
SizeOf(Buf) у массива Char размер вдвое больше буфера Length(Buf)
строка как буфер байтов байты меняются при преобразовании TBytes
PChar в объявлении чужой DLL DLL получает UTF-16 вместо ANSI PAnsiChar и AnsiString
Ch in ['a'..'z'] предупреждение W1050 CharInSet

Ловушки

  • Хеши и подписи. MD5 от строки теперь считается по другим байтам, и старые хеши паролей не совпадут. Кодировку для хеша фиксируют явно — ту, что была.
  • Арифметика через PChar. Шаг указателя стал 2 байта. Для арифметики над байтами — PByte.
  • Сторонние компоненты. Версия для Delphi 6 либо не соберётся, либо соберётся с теми же ошибками внутри.

Правило

Внутри программы — string, на границе — явная кодировка, двоичные данные — TBytes.

Под капотом

  • У UnicodeString и AnsiString общий заголовок: кодовая страница, размер элемента, счётчик ссылок, длина. UTF8String — это AnsiString(65001), а RawByteString принимает любую AnsiString без перекодирования.
  • Неявное преобразование вставляет компилятор. В коде его не видно, оно стоит аллокации и копирования, а при сужении до ANSI заменяет непредставимые символы на ?.
  • Length считает элементы UTF-16. Символ вне базовой плоскости, например эмодзи, занимает два Char — суррогатную пару.

Цена

  • Ревизия всех границ: файлы, потоки, сокеты, буферы WinAPI, DLL, BLOB-поля, хеши.
  • Строки с кириллицей и латиницей занимают вдвое больше памяти; для настольного клиента это обычно незаметно.

Компромиссы

  • Быстрый путь — объявить проблемные строки AnsiString. Работает, но ANSI расползается дальше, а каждый вызов VCL добавляет преобразование.
  • Правильный путь — Unicode внутри и явная кодировка на границе. Дольше, зато долг не остаётся.
  • Внешние форматы не меняют молча. Если партнёр ждёт файл в 1251, пишут 1251 через TEncoding.GetEncoding(1251).

Как проверить в проде

  1. Включить в проекте предупреждения W1057–W1060 и разобрать каждое.
  2. Прогнать сценарии с текстом вне кодовой страницы 1251: иероглифы, эмодзи.
  3. Сравнить побайтно файлы и выгрузки, которые записали старая и новая версии.

Итог: Unicode-миграция — это поиск всех границ, где текст становится байтами, и явная кодировка на каждой из них.

Delphi 6: строка как байты
// string = AnsiString: 1 символ = 1 байт
procedure SaveText(Stream: TStream; const S: string);
begin
Stream.WriteBuffer(S[1], Length(S)); // в Unicode запишет половину строки
end;
function WinDir: string;
var
Buf: array[0..MAX_PATH] of Char;
begin
// в Unicode SizeOf(Buf) вдвое больше, чем символов в буфере
SetString(Result, Buf, GetWindowsDirectory(Buf, SizeOf(Buf)));
end;
// старая DLL ждёт ANSI, а в Unicode PChar = PWideChar
function LegacyCheck(Code: PChar): Integer; stdcall; external 'legacy.dll';
Delphi 12: явная кодировка на границе
procedure SaveText(Stream: TStream; const S: string);
var
Bytes: TBytes;
begin
Bytes := TEncoding.UTF8.GetBytes(S); // кодировка задана явно
if Length(Bytes) > 0 then
Stream.WriteBuffer(Bytes[0], Length(Bytes));
end;
function WinDir: string;
var
Buf: array[0..MAX_PATH] of Char;
begin
SetString(Result, Buf, GetWindowsDirectory(Buf, Length(Buf))); // размер в символах
end;
function LegacyCheck(Code: PAnsiChar): Integer; stdcall; external 'legacy.dll';
// вызов: LegacyCheck(PAnsiChar(AnsiString(S)))

AnsiString(S) перекодирует строку в кодовую страницу системы. Если DLL ждёт конкретную кодировку, объявите тип AnsiString(1251). Для старого формата файла вместо TEncoding.UTF8 берут TEncoding.GetEncoding(1251); этот экземпляр нужно освободить.

Middle+

Как перевести слой данных с ADO на FireDAC и что проверить?

Заголовок раздела «Как перевести слой данных с ADO на FireDAC и что проверить?»
Ответ

Что это

FireDAC — основная библиотека доступа к базам данных в современных Delphi. Компоненты ADO (dbGo) по-прежнему входят в поставку, но новые возможности получает FireDAC.

Соответствие компонентов

ADO (dbGo) FireDAC
TADOConnection TFDConnection
TADOQuery, TADODataSet TFDQuery
TADOStoredProc TFDStoredProc
TADOCommand TFDCommand
TADOTable TFDTable

Как пользоваться

  1. Заменить компоненты в .pas и .dfm — вручную или утилитой reFind.
  2. Настроить TFDConnection на MS SQL Server.
  3. Открыть каждую форму и проверить поля и параметры.

Частая ошибка

Считать, что после замены имён всё работает. Если тип поля изменился, форма упадёт при открытии с ошибкой Type mismatch for field.

Как устроено

Что ADO FireDAC
Параметры процедуры Parameters, Direction Params, ParamType
Параметры из метаданных Parameters.Refresh Prepare при fiMeta в FetchOptions.Items
Транзакции BeginTrans, CommitTrans StartTransaction, Commit

Что проверить — по шагам

  1. Типы полей. nvarchar → TWideStringField, nvarchar(max) → TWideMemoField. Persistent-поля в .dfm должны совпасть с тем, что вернёт FireDAC; расхождения закрывают MapRules.
  2. Параметры процедур. Имя, тип, направление, размер. Значения по умолчанию из объявления процедуры FireDAC не поддерживает — передавайте каждый параметр.
  3. Строки и NULL. StrsTrim (по умолчанию True) обрезает хвостовые пробелы у CHAR/NCHAR, StrsEmpty2Null превращает пустую строку в NULL.
  4. Дата и время. datetime2, date и time со старым ODBC-драйвером приходят строками — нужен современный драйвер.
  5. Автоинкремент. IDENTITY распознаётся как TFDAutoIncField; проверьте, что новое значение видно после Post.
  6. Транзакции. По умолчанию FireDAC работает в автокоммите; код с явными транзакциями переводится на StartTransaction/Commit/Rollback.

Ловушки

  • ResourceOptions.UnifyParams = True убирает @ из имён параметров, и ParamByName('@Id') перестанет находить параметр.
  • Триггер без SET NOCOUNT ON даёт ошибку Update command updated [N] instead of [1] record.

Правило

Различия закрывайте настройками соединения, а не правками форм.

Под капотом

  • FireDAC работает с MS SQL Server через ODBC-драйвер Microsoft, ADO — через OLE DB-провайдер. Отсюда разные типы полей и параметров для одних и тех же колонок.
  • Правила MapRules применяются при подготовке команды, срабатывает первое подходящее. Задав их на TFDConnection, вы меняете поведение всех наборов данных сразу.
  • Параметры из метаданных стоят запроса к серверу при Prepare. Без fiMeta их описывают вручную — так делают, если метаданные врут, например у VARCHAR без размера.

Почему нужна автоматическая сверка

  1. Снять с обеих версий описание каждого вызова: процедура, параметры с типом, направлением и размером.
  2. Нормализовать различия: pdInput ↔ ptInput, pdReturnValue ↔ ptResult, @ в именах.
  3. Сравнить diff-ом и разобрать каждое расхождение.
  4. Прогнать сценарии на копии боевой базы и сравнить результаты и данные после записи.

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

Цена

  • Инструмент сверки и копия базы для прогонов.
  • ODBC Driver 18 по умолчанию шифрует соединение и проверяет сертификат сервера — это инфраструктурная работа до первого запуска.

Компромиссы

  • Сначала Delphi 12 с ADO, потом FireDAC: дольше, но каждый этап проверяется отдельно.
  • MapRules для совместимости со старыми типами быстрее, чем пересоздание persistent-полей, но закрепляет старые типы в новом коде.

Как проверить в проде

Трассировка SQL Server (Extended Events) на одном сценарии в старой и новой версиях показывает, какие вызовы и с какими параметрами пришли на сервер.

Итог: FireDAC меняет семантику доступа к данным, поэтому эквивалентность доказывают автоматической сверкой, а не ручной проверкой.

Delphi 6: ADO
ADOStoredProc1.ProcedureName := 'dbo.GetOrderTotal';
ADOStoredProc1.Parameters.Refresh; // параметры из метаданных сервера
ADOStoredProc1.Parameters.ParamByName('@OrderId').Value := OrderId;
ADOStoredProc1.ExecProc;
Total := ADOStoredProc1.Parameters.ParamByName('@Total').Value;
Delphi 12: FireDAC
// Общая настройка соединения: datetime как TDateTimeField, пробелы CHAR не обрезать
FDConnection1.FormatOptions.OwnMapRules := True;
with FDConnection1.FormatOptions.MapRules.Add do
begin
SourceDataType := dtDateTimeStamp;
TargetDataType := dtDateTime;
end;
FDConnection1.FormatOptions.StrsTrim := False;
FDStoredProc1.StoredProcName := 'dbo.GetOrderTotal';
FDStoredProc1.Prepare; // при fiMeta в FetchOptions.Items Params заполняются сами
FDStoredProc1.ParamByName('@OrderId').AsInteger := OrderId;
FDStoredProc1.ExecProc;
Total := FDStoredProc1.ParamByName('@Total').AsCurrency;

Правило dtDateTimeStamp → dtDateTime и StrsTrim := False взяты из официального примера миграции с BDE. Нужны ли они вам, решает сверка типов полей и данных старой и новой версий.

Middle+
Ответ

Что это

64-битная программа может адресовать больше памяти, и каждый указатель в ней занимает 8 байт вместо 4.

Что меняется

Тип Win32 Win64
Pointer, ссылки на объекты 4 байта 8 байт
NativeInt, NativeUInt 4 байта 8 байт
Integer, Cardinal, LongInt 4 байта 4 байта

Как пользоваться

  • Указатель или объект в целом числе — только NativeInt: Tag := NativeInt(Obj).
  • Размер указателя — SizeOf(Pointer), а не число 4.
  • Платформа Windows 64-bit добавляется в Project Manager; Win32 и Win64 собираются в разные EXE.

Частая ошибка

Integer(Obj) компилируется и в Win64, но отрезает старшие 4 байта адреса. Ошибка проявится, только когда адрес окажется выше 4 ГБ, поэтому тесты её часто не ловят.

Как устроено

  • Указатели, NativeInt, THandle, HWND, WPARAM, LPARAM, LRESULT — 8 байт; Integer и LongInt на Windows — 4 байта.
  • Extended в Win64 — псевдоним Double: 8 байт вместо 10. Точные расчёты могут дать другой последний знак, а двоичные файлы с Extended станут несовместимы.
  • Смешивать ассемблер и Pascal в одной функции нельзя: функция либо целиком на asm, либо на Pascal.

Было — стало

Было Стало
Tag := Integer(Obj) Tag := NativeInt(Obj)
SendMessage(..., Integer(@Buf)) LPARAM(@Buf)
SetWindowLong(..., GWL_WNDPROC, ...) SetWindowLongPtr(..., GWLP_WNDPROC, ...)
Message.Result := Integer(Self) LRESULT(Self)
Count * 4 для массива указателей Count * SizeOf(Pointer)

Ловушки

  • Зависимости. DLL, .obj-файлы и внутрипроцессные COM-серверы нужны в 64-битной версии. Внепроцессный COM-сервер (EXE) работает с любой разрядностью.
  • Компоненты. Пакеты для дизайнера нужны в версии под разрядность IDE, а для сборки — в 64-битной.
  • Драйвер БД. Для FireDAC нужен 64-битный ODBC-драйвер SQL Server, для ADO — 64-битный OLE DB-провайдер.

Правило

Сначала инвентаризация зависимостей, потом правка кода.

Под капотом

  • 32-битный процесс на 64-битной Windows работает через WOW64 и получает 2 ГБ адресного пространства, а с флагом IMAGE_FILE_LARGE_ADDRESS_AWARE — до 4 ГБ. В Delphi флаг ставит директива {$SetPEFlags IMAGE_FILE_LARGE_ADDRESS_AWARE}.
  • В процессе одна разрядность: 32-битную DLL нельзя загрузить в 64-битный процесс, и наоборот.
  • Патчи функций во время выполнения и ассемблер под IA-32 переписываются под x64 вручную.

Когда 64 бита не нужны

  • Клиенту хватает памяти: формы, гриды и отчёты укладываются в сотни мегабайт.
  • У ключевой зависимости нет 64-битной версии: генератор отчётов, драйвер оборудования, COM-библиотека.

64 бита нужны, когда клиент упирается в память или когда зависимость выходит только в 64-битной версии.

Компромиссы

  • Две платформы удваивают сборки и тестирование. Если переход нужен, его делают отдельным этапом после Unicode и FireDAC.
  • 32-битную зависимость без замены выносят в отдельный процесс-посредник и вызывают через межпроцессное взаимодействие.

Как проверить в проде

  • Тестовая машина с настройкой реестра AllocationPreference = 0x100000: Windows выделяет память сверху вниз, и усечённые указатели падают сразу, а не после 4 ГБ.
  • Сравнение результатов расчётов Win32 и Win64 на одних данных ловит расхождения из-за Extended.

Итог: решение о 64 битах принимают по зависимостям и памяти, а не по моде; если клиенту это ничего не даёт, переход откладывают.

Delphi 6, Win32: указатель в Integer
ListView1.Tag := Integer(Customer); // в Win64 адрес обрежется
SendMessage(Edit1.Handle, WM_SETTEXT, 0, Integer(PChar(Title)));
OldProc := Pointer(SetWindowLong(Handle, GWL_WNDPROC, Integer(@NewWndProc)));
P := Pointer(Integer(P) + 10); // арифметика через Integer
Move(Src^, Dst^, Count * 4); // 4 — «размер указателя»
Delphi 12: Win32 и Win64
ListView1.Tag := NativeInt(Customer); // Tag имеет тип NativeInt
SendMessage(Edit1.Handle, WM_SETTEXT, 0, LPARAM(PChar(Title)));
OldProc := Pointer(SetWindowLongPtr(Handle, GWLP_WNDPROC, LONG_PTR(@NewWndProc)));
P := PByte(P) + 10; // PByte поддерживает арифметику указателей
Move(Src^, Dst^, Count * SizeOf(Pointer));
Senior+

Как спланировать миграцию большого Delphi-клиента без остановки бизнеса?

Заголовок раздела «Как спланировать миграцию большого Delphi-клиента без остановки бизнеса?»
Ответ

Что это

Миграция клиента — перевод того же приложения на новую версию Delphi с сохранением поведения. Пользователь должен получить те же формы, расчёты и отчёты.

Как пользоваться: этапы

  1. Составить список модулей, форм и сторонних компонентов.
  2. Найти замену компонентам, которых нет для новой Delphi.
  3. Собрать проект в новой версии и исправить ошибки компиляции.
  4. Проверить формы и отчёты, сравнивая их со старой версией.
  5. Отдать новую версию небольшой группе пользователей, старую оставить для отката.

Частая ошибка

Попутно улучшать код и интерфейс. Тогда при расхождении непонятно, что виновато — миграция или улучшение.

Инвентаризация

Что Что выяснить
Модули и формы сколько, кто владелец, как часто используются
Сторонние компоненты есть ли версия для Delphi 12/13 и исходники; чем заменить (RX Library — JVCL или стандартные VCL)
Доступ к данным все запросы и вызовы процедур с параметрами
Внешние зависимости DLL, COM, отчёты, оборудование

Когда что выбирать

  • Заморозка фич. Проще всего, но бизнес ждёт. Подходит, если перенос занимает недели.
  • Две ветки. Новые фичи идут в старую ветку и регулярно переносятся в новую. Нужны частые слияния и автоматическая проверка расхождений.
  • Общий код для двух компиляторов. Различия закрываются условной компиляцией ({$IFDEF UNICODE}). Ветки не расходятся, но код обрастает условиями.

Ловушки

  • Новая IDE при сохранении дописывает в .dfm свойства вроде ExplicitWidth, поэтому формы сравнивают после нормализации.
  • Двоичные .dfm сначала переводят в текст утилитой convert.exe из поставки Delphi.
  • Формы на мониторах с высоким DPI масштабируются иначе, чем в Delphi 6.

Правило

Один этап — один тип изменений, и у каждого этапа есть автоматическая проверка и откат.

Этапы

  1. Инвентаризация и оценка: компоненты, процедуры, зависимости, критичные для бизнеса формы.
  2. Сборка в Delphi 12/13 со старым ADO: Unicode, замена компонентов, без изменения поведения.
  3. Переход с ADO на FireDAC под автоматической сверкой.
  4. По необходимости — 64 бита, стили VCL, высокий DPI.

Автоматизированные проверки

  • Формы. Сравнение .dfm: состав компонентов, свойства, обработчики событий.
  • Маппинг. Persistent-поля и их типы, DataField и DataSource у контролов.
  • Вызовы процедур. Имя и параметры с типом, направлением и размером, затем результаты на копии боевой базы.
  • Поведение. Характеризующие тесты для расчётов: входные данные из прода, эталон — результат старой версии.

Цена

  • Две ветки или общий код с условиями на всё время миграции.
  • Инструменты сверки, копия боевой базы и время пользователей на приёмку.

Компромиссы

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

Как проверить в проде: готовность и откат

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

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

Снимок параметров: ADO (Delphi 6)
uses SysUtils, TypInfo, DB, ADODB;
// Описание параметров процедуры для сверки со снимком новой версии
function DescribeParams(Proc: TADOStoredProc): string;
var
I: Integer;
P: TParameter;
begin
Result := Proc.ProcedureName;
for I := 0 to Proc.Parameters.Count - 1 do
begin
P := Proc.Parameters[I];
Result := Result + sLineBreak + Format('%s %s %s %d', [P.Name,
GetEnumName(TypeInfo(TParameterDirection), Ord(P.Direction)),
GetEnumName(TypeInfo(TFieldType), Ord(P.DataType)), P.Size]);
end;
end;
Снимок параметров: FireDAC (Delphi 12)
uses System.SysUtils, System.TypInfo, Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Param;
function DescribeParams(Proc: TFDStoredProc): string;
var
I: Integer;
P: TFDParam;
begin
Result := Proc.StoredProcName;
for I := 0 to Proc.Params.Count - 1 do
begin
P := Proc.Params[I];
Result := Result + sLineBreak + Format('%s %s %s %d', [P.Name,
GetEnumName(TypeInfo(TParamType), Ord(P.ParamType)),
GetEnumName(TypeInfo(TFieldType), Ord(P.DataType)), P.Size]);
end;
end;

Снимки обеих версий нормализуют (pdInput ↔ ptInput, pdReturnValue ↔ ptResult) и сравнивают обычным diff. Каждая строка расхождения — повод разобраться до выпуска.

Senior+

Как постепенно переносить функции из Delphi-клиента в .NET-сервисы?

Заголовок раздела «Как постепенно переносить функции из Delphi-клиента в .NET-сервисы?»
Ответ

Что это

Постепенный перенос: новая система растёт рядом со старой и забирает функции одну за другой. Подробнее о паттерне — в теме Архитектурные стили.

Как пользоваться

  1. Выбрать небольшую функцию: справочник, расчёт, отчёт.
  2. Реализовать её в .NET-сервисе с HTTP API.
  3. В Delphi-клиенте заменить SQL-запрос вызовом сервиса.
  4. Сравнить результаты старого и нового пути.
  5. Удалить старый код из клиента.

Частая ошибка

Скопировать правило в сервис и оставить его в клиенте «на всякий случай». Через месяц правку внесут только в одно место, и ответы разойдутся.

Где провести границу

Где сейчас логика Что делать
Хранимые процедуры оставить в БД, сервис их вызывает; переписывать позже
Обработчики форм в клиенте переносить в сервис — главный выигрыш
UI и ввод данных остаются в Delphi

Как устроено

  • Клиент обращается к сервису через THTTPClient (System.Net.HttpClient) или компоненты TRESTClient, TRESTRequest, TRESTResponse из REST Client Library.
  • Общая база — переходное состояние: сервис и клиент работают с одними таблицами, но писать в таблицу может только один из них.
  • Читающие функции переносить проще: они не меняют данные, а результат легко сравнить.

Ловушки

  • Двойная логика. Правило живёт в двух местах; у старого пути должен быть срок удаления.
  • Синхронный вызов в UI-потоке. Форма замирает, пока сервис отвечает. Нужны таймауты и вызов в фоне (TTask.Run) с возвратом в UI через TThread.Queue.
  • Недоступный сервис. Клиент должен понятно сообщить об этом, а не упасть с исключением.

Правило

Одно правило — один владелец, одна таблица — один писатель.

Компромиссы границы

  • Логика в процедурах уже общая для всех клиентов. Переносить её в сервис стоит ради тестов, масштабирования или другого источника данных; иначе сервис её просто вызывает.
  • Своя база сервиса появляется, когда в таблицы не пишет никто, кроме него. До этого — общая база через ACL, чтобы старая схема не протекала в новую модель.

Под капотом вызова

  • Аутентификация пользователя клиента в сервисе — отдельная задача: доменная учётная запись Windows или токен.
  • Старые версии клиента живут долго, поэтому контракт API меняется только совместимо или через версию.

Цена

  • Сетевые задержки и отказы там, где раньше был локальный SQL.
  • Две реализации и их сравнение в переходный период.

Как проверить в проде

  • Параллельный запуск для расчётов. Клиент считает по-старому и вызывает сервис, расхождения пишутся в журнал, пользователь видит старый результат.
  • Метрики сервиса. Задержка и ошибки по эндпоинтам, версия клиента в заголовке User-Agent — чтобы знать, когда старый путь можно удалить.
  • Откат. Флаг по филиалу или группе пользователей возвращает старый путь без нового релиза.

Итог: перенос в .NET — это strangler fig с явной границей, одним владельцем каждого правила и таблицы и сравнением результатов до удаления старого пути.

До: клиент считает сам
// Остаток считает клиент прямым SQL к общей базе
function GetStock(const Sku: string): Integer;
begin
StockQuery.SQL.Text := 'SELECT SUM(Qty) FROM StockMoves WHERE Sku = :Sku';
StockQuery.Parameters.ParamByName('Sku').Value := Sku;
StockQuery.Open;
try
Result := StockQuery.Fields[0].AsInteger;
finally
StockQuery.Close;
end;
end;
После: клиент вызывает .NET-сервис
uses System.SysUtils, System.JSON, System.NetEncoding, System.Net.HttpClient;
function GetStock(const Sku: string): Integer;
var
Client: THTTPClient;
Resp: IHTTPResponse;
Json: TJSONValue;
begin
Client := THTTPClient.Create;
try
Client.ConnectionTimeout := 3000; // мс; по умолчанию минута
Client.ResponseTimeout := 5000;
Resp := Client.Get('https://stock.internal/api/stock?sku=' + TNetEncoding.URL.Encode(Sku));
if Resp.StatusCode <> 200 then
raise Exception.CreateFmt('Stock service returned HTTP %d', [Resp.StatusCode]);
Json := TJSONObject.ParseJSONValue(Resp.ContentAsString(TEncoding.UTF8));
if Json = nil then
raise Exception.Create('Stock service returned invalid JSON');
try
Result := Json.GetValue<Integer>('quantity');
finally
Json.Free;
end;
finally
Client.Free;
end;
end;

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