Перейти до вмісту

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

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

Перед цією темою: Object Pascal, VCL і доступ до даних.

Middle+

Що зламалося під час переходу на Unicode у Delphi 2009 і як це виправляти?

Section titled “Що зламалося під час переходу на 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 і що перевірити?

Section titled “Як перевести шар даних з 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 біти?

Section titled “Що треба врахувати під час переходу на 64 біти?”
Відповідь

Що це

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-клієнта без зупинки бізнесу?

Section titled “Як спланувати міграцію великого 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-сервіси?

Section titled “Як поступово переносити функції з 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;

Сигнатура функції не змінилася, тому форми не чіпають. Перемикання між старою й новою реалізацією зручно сховати за прапорцем.