Миграция Delphi 6 → 12/13
Как перевести большой клиент с Delphi 6 на Delphi 12/13: что ломает Unicode, чем FireDAC отличается от ADO, что меняется в 64 битах, как спланировать миграцию без остановки бизнеса и как постепенно переносить функции в .NET-сервисы. На собеседовании здесь проверяют, знаете ли вы, где миграция тихо портит данные и как поймать это раньше пользователей.
Перед этой темой: Object Pascal, VCL и доступ к данным.
Что сломалось при переходе на 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).
Как проверить в проде
- Включить в проекте предупреждения 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 и что проверить?
Заголовок раздела «Как перевести слой данных с 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 бита?
Заголовок раздела «Что нужно учесть при переходе на 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-клиента без остановки бизнеса?
Заголовок раздела «Как спланировать миграцию большого 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-сервисы?
Заголовок раздела «Как постепенно переносить функции из 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 и настраивается в дизайнере.- Когда удалять старый путь в клиенте? — Когда старых версий клиента нет в метриках, а параллельный запуск не показывает расхождений полный бизнес-цикл.