Міграція Delphi 6 → 12/13
Як перевести великий клієнт з Delphi 6 на Delphi 12/13: що ламає Unicode, чим FireDAC відрізняється від ADO, що змінюється в 64 бітах, як спланувати міграцію без зупинки бізнесу і як поступово переносити функції в .NET-сервіси. На співбесіді тут перевіряють, чи знаєте ви, де міграція тихо псує дані і як помітити це раніше за користувачів.
Перед цією темою: Object Pascal, VCL і доступ до даних.
Що зламалося під час переходу на 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).
Як перевірити в продакшені
- Увімкнути в проєкті попередження W1057–W1060 і розібрати кожне.
- Прогнати сценарії з текстом поза кодовою сторінкою 1251: ієрогліфи, емодзі.
- Порівняти побайтово файли й вивантаження, які записали стара й нова версії.
Підсумок: Unicode-міграція — це пошук усіх меж, де текст стає байтами, і явне кодування на кожній із них.
// 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 = PWideCharfunction LegacyCheck(Code: PChar): Integer; stdcall; external 'legacy.dll';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); цей екземпляр треба звільнити.
- Чим
RawByteStringвідрізняється відAnsiString? — Не перекодовує під час присвоєння; годиться як параметр, що приймає рядок у будь-якій кодовій сторінці. - Скільки
Charзаймає емодзі? — Два: сурогатна пара UTF-16, томуLengthне дорівнює кількості видимих символів. - Як
TStringList.LoadFromFileчитає старі ANSI-файли? — За BOM, якщо він є; без BOM — у кодуванніDefaultEncoding, за замовчуванням це ANSI-сторінка системи. - Як зберегти сумісність хешів паролів? — Рахувати хеш від байтів у старому кодуванні:
TEncoding.GetEncoding(1251).GetBytes(...). - Що робити зі стороннім компонентом без Unicode-версії? — Оновити, замінити аналогом або портувати за вихідним кодом; покинутий компонент — постійний ризик.
Як перевести шар даних з 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 |
Як користуватися
- Замінити компоненти в .pas і .dfm — вручну або утилітою
reFind. - Налаштувати
TFDConnectionна MS SQL Server. - Відкрити кожну форму й перевірити поля та параметри.
Типова помилка
Вважати, що після заміни імен усе працює. Якщо тип поля змінився, форма впаде під час відкриття з помилкою Type mismatch for field.
Як влаштовано
| Що | ADO | FireDAC |
|---|---|---|
| Параметри процедури | Parameters, Direction |
Params, ParamType |
| Параметри з метаданих | Parameters.Refresh |
Prepare за fiMeta у FetchOptions.Items |
| Транзакції | BeginTrans, CommitTrans |
StartTransaction, Commit |
Що перевірити — крок за кроком
- Типи полів.
nvarchar→TWideStringField,nvarchar(max)→TWideMemoField. Persistent-поля в .dfm мають збігатися з тим, що поверне FireDAC; розбіжності закриваютьMapRules. - Параметри процедур. Ім’я, тип, напрямок, розмір. Значення за замовчуванням з оголошення процедури FireDAC не підтримує — передавайте кожен параметр.
- Рядки й NULL.
StrsTrim(за замовчуваннямTrue) обрізає кінцеві пробіли вCHAR/NCHAR,StrsEmpty2Nullперетворює порожній рядок на NULL. - Дата й час.
datetime2,dateіtimeзі старим ODBC-драйвером приходять рядками — потрібен сучасний драйвер. - Автоінкремент.
IDENTITYрозпізнається якTFDAutoIncField; перевірте, що нове значення видно післяPost. - Транзакції. За замовчуванням 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без розміру.
Навіщо автоматична звірка
- Зняти з обох версій опис кожного виклику: процедура, параметри з типом, напрямком і розміром.
- Нормалізувати відмінності:
pdInput↔ptInput,pdReturnValue↔ptResult,@в іменах. - Порівняти diff-ом і розібрати кожну розбіжність.
- Прогнати сценарії на копії бойової бази й порівняти результати та дані після запису.
Ручна перевірка тисячі форм вибіркова, а звірка повторюється після кожної зміни.
Ціна
- Інструмент звірки й копія бази для прогонів.
- ODBC Driver 18 за замовчуванням шифрує з’єднання й перевіряє сертифікат сервера — це інфраструктурна робота до першого запуску.
Компроміси
- Спершу Delphi 12 з ADO, потім FireDAC: довше, але кожен етап перевіряється окремо.
MapRulesдля сумісності зі старими типами швидші, ніж перестворення persistent-полів, але закріплюють старі типи в новому коді.
Як перевірити в продакшені
Трасування SQL Server (Extended Events) на одному сценарії в старій і новій версіях показує, які виклики й з якими параметрами прийшли на сервер.
Підсумок: FireDAC змінює семантику доступу до даних, тому еквівалентність доводять автоматичною звіркою, а не ручною перевіркою.
ADOStoredProc1.ProcedureName := 'dbo.GetOrderTotal';ADOStoredProc1.Parameters.Refresh; // параметри з метаданих сервераADOStoredProc1.Parameters.ParamByName('@OrderId').Value := OrderId;ADOStoredProc1.ExecProc;Total := ADOStoredProc1.Parameters.ParamByName('@Total').Value;// Спільне налаштування з'єднання: datetime як TDateTimeField, пробіли CHAR не обрізатиFDConnection1.FormatOptions.OwnMapRules := True;with FDConnection1.FormatOptions.MapRules.Add dobegin 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. Чи потрібні вони вам, вирішує звірка типів полів і даних старої та нової версій.
- Що робить
reFind? — Замінює імена компонентів і властивостей у .pas і .dfm за скриптом регулярних виразів; для dbGo є скрипт і модульFireDAC.ADO.Migrate.pas. - Чому колонка
datetime2прийшла рядком? — Старий ODBC-драйвер SQL Server не знає типів SQL Server 2008; поставте сучасний драйвер. - Що буде з
ParamByName('@Id')післяUnifyParams := True? — Параметр не знайдеться: префікс@прибрано з імен. - Чому процедура падає з
Data too large for variable? — Параметр оголошено якVARCHARбез розміру; приберітьfiMetaі задайтеSizeвручну. - Чи потрібна особлива редакція Delphi? — За матрицею редакцій клієнт-серверні драйвери FireDAC, зокрема MS SQL, входять в Enterprise і вище або в окремий пакет для Professional.
Що треба врахувати під час переходу на 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 біти ухвалюють за залежностями й пам’яттю, а не за модою; якщо клієнту це нічого не дає, перехід відкладають.
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); // арифметика через IntegerMove(Src^, Dst^, Count * 4); // 4 — «розмір вказівника»ListView1.Tag := NativeInt(Customer); // Tag має тип NativeIntSendMessage(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));- Скільки байтів займає
Integerу Win64? — 4; 8 байтів уNativeInt, вказівників і дескрипторів. - Чому результати розрахунків розійшлися між Win32 і Win64? —
Extendedу Win64 — цеDouble, точність нижча. - Чи можна з 64-бітного клієнта викликати 32-бітну DLL? — Не у своєму процесі; лише через окремий 32-бітний процес-посередник.
- Що дає
LARGEADDRESSAWARE? — 32-бітному процесу на 64-бітній Windows доступно до 4 ГБ замість 2. - Чи працює асемблер у Win64? — Лише функції повністю на
asm; змішувати з Pascal не можна, код переписують під x64.
Як спланувати міграцію великого Delphi-клієнта без зупинки бізнесу?
Section titled “Як спланувати міграцію великого Delphi-клієнта без зупинки бізнесу?”Що це
Міграція клієнта — перенесення того самого застосунку на нову версію Delphi зі збереженням поведінки. Користувач має отримати ті самі форми, розрахунки й звіти.
Як користуватися: етапи
- Скласти список модулів, форм і сторонніх компонентів.
- Знайти заміну компонентам, яких немає для нової Delphi.
- Зібрати проєкт у новій версії й виправити помилки компіляції.
- Перевірити форми й звіти, порівнюючи їх зі старою версією.
- Віддати нову версію невеликій групі користувачів, стару залишити для відкату.
Типова помилка
Принагідно покращувати код та інтерфейс. Тоді за розбіжності незрозуміло, що винне — міграція чи покращення.
Інвентаризація
| Що | Що з’ясувати |
|---|---|
| Модулі й форми | скільки, хто власник, як часто використовуються |
| Сторонні компоненти | чи є версія для Delphi 12/13 і вихідний код; чим замінити (RX Library — JVCL або стандартні VCL) |
| Доступ до даних | усі запити й виклики процедур з параметрами |
| Зовнішні залежності | DLL, COM, звіти, обладнання |
Коли що обирати
- Заморожування фіч. Найпростіше, але бізнес чекає. Підходить, якщо перенесення займає тижні.
- Дві гілки. Нові фічі йдуть у стару гілку й регулярно переносяться в нову. Потрібні часті злиття й автоматична перевірка розбіжностей.
- Спільний код для двох компіляторів. Відмінності закриваються умовною компіляцією (
{$IFDEF UNICODE}). Гілки не розходяться, але код обростає умовами.
Пастки
- Нова IDE під час збереження дописує в .dfm властивості на кшталт
ExplicitWidth, тому форми порівнюють після нормалізації. - Двійкові .dfm спершу переводять у текст утилітою
convert.exeз постачання Delphi. - Форми на моніторах з високим DPI масштабуються інакше, ніж у Delphi 6.
Правило
Один етап — один тип змін, і в кожного етапу є автоматична перевірка та відкат.
Етапи
- Інвентаризація й оцінка: компоненти, процедури, залежності, критичні для бізнесу форми.
- Збирання в Delphi 12/13 зі старим ADO: Unicode, заміна компонентів, без зміни поведінки.
- Перехід з ADO на FireDAC під автоматичною звіркою.
- За потреби — 64 біти, стилі VCL, високий DPI.
Автоматизовані перевірки
- Форми. Порівняння .dfm: склад компонентів, властивості, обробники подій.
- Маппінг. Persistent-поля та їхні типи,
DataFieldіDataSourceу контролів. - Виклики процедур. Ім’я й параметри з типом, напрямком і розміром, потім результати на копії бойової бази.
- Поведінка. Характеризаційні тести для розрахунків: вхідні дані з продакшену, еталон — результат старої версії.
Ціна
- Дві гілки або спільний код з умовами на весь час міграції.
- Інструменти звірки, копія бойової бази й час користувачів на приймання.
Компроміси
- Усе одразу проти поетапно. Один великий реліз простіше планувати, але відкочувати доводиться всю версію. Поетапно довше, зате кожен крок маленький.
- Паралельна експлуатація. Обидві версії працюють з однією базою, пілотна група — на новій. Схема БД змінюється лише сумісно, інакше відкат неможливий.
Як перевірити в продакшені: готовність і відкат
- Пілотна група відпрацювала повний бізнес-цикл, включно із закриттям місяця.
- Нових помилок у журналі немає, звернень у підтримку не побільшало.
- Відкат — запуск старого EXE; він можливий, доки схема сумісна з обома версіями.
Підсумок: міграція без зупинки бізнесу — це маленькі оборотні етапи, автоматична звірка зі старою версією й наперед задані критерії готовності.
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;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. Кожен рядок розбіжності — привід розібратися до випуску.
- Чому не перейти одразу на FireDAC разом із Delphi 12? — Дві причини розбіжностей одночасно: незрозуміло, що зламало поведінку.
- Чим замінити RX Library? — Її компоненти увійшли в JVCL; частину закривають стандартні компоненти VCL.
- Як зрозуміти, що нова версія готова? — Пілот пройшов повний бізнес-цикл, звірка без розбіжностей, помилок і звернень не більше, ніж зі старою.
- Як відкотитися? — Повернути старий EXE; тому схему БД під час міграції змінюють лише сумісно.
- Що робити з фічами, поки триває міграція? — Заморозити, вести дві гілки або спільний код з умовною компіляцією; вибір залежить від тривалості міграції.
Як поступово переносити функції з Delphi-клієнта в .NET-сервіси?
Section titled “Як поступово переносити функції з Delphi-клієнта в .NET-сервіси?”Що це
Поступове перенесення: нова система росте поруч зі старою й забирає функції одну за одною. Докладніше про патерн — у темі Архітектурні стилі.
Як користуватися
- Вибрати невелику функцію: довідник, розрахунок, звіт.
- Реалізувати її в .NET-сервісі з HTTP API.
- У Delphi-клієнті замінити SQL-запит викликом сервісу.
- Порівняти результати старого й нового шляху.
- Видалити старий код із клієнта.
Типова помилка
Скопіювати правило в сервіс і залишити його в клієнті «про всяк випадок». За місяць правку внесуть лише в одне місце, і відповіді розійдуться.
Де провести межу
| Де зараз логіка | Що робити |
|---|---|
| Збережені процедури | лишити в БД, сервіс їх викликає; переписувати пізніше |
| Обробники форм у клієнті | переносити в сервіс — головний виграш |
| 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;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;Сигнатура функції не змінилася, тому форми не чіпають. Перемикання між старою й новою реалізацією зручно сховати за прапорцем.
- Де має жити бізнес-логіка після міграції? — У сервісі; процедури БД лишаються там, де вони дають спільну точку для всіх клієнтів, доки їх не перенесли.
- Чи можна сервісу писати в таблиці, у які пише клієнт? — Як перехідний стан — лише з одним, хто пише, на таблицю; інакше конфлікти й подвійна логіка.
- Як не заморозити форму на час HTTP-виклику? — Виклик у
TTask.Run, оновлення UI черезTThread.Queue, плюс таймаути. THTTPClientчиTRESTClient? —THTTPClient— низькорівневий і без компонентів на формі;TRESTClientзручніший для JSON і налаштовується в дизайнері.- Коли видаляти старий шлях у клієнті? — Коли старих версій клієнта немає в метриках, а паралельний запуск не показує розбіжностей повний бізнес-цикл.