Практический пример: устранение ошибок пакетной локализации

Практический пример: устранение ошибок пакетной локализации

1. Введение

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

2. Предпосылки проблемы

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

На стороне frontend задача заключалась лишь в отображении названия отдела и информации о роли, возвращаемых backend. Однако в ходе разработки мы заметили, что названия некоторых отделов возвращались как пустые значения. Сначала мы предположили, что проблема связана с сопоставлением данных на frontend или с обработкой данных ответа. Но после проверки ответа API выяснилось, что frontend не исключал эти значения: пустыми были сами названия отделов, возвращаемые backend.

Затем мы проверили логику запроса на backend. Она получала название отдела на основе ko — языка текущего экрана. Само это поведение было нормальным. Проблема заключалась в мультиязычных данных, хранившихся в DB. При непосредственной проверке DB мы обнаружили, что корейские названия отделов были сохранены под кодом языка en вместо ko.

Иными словами, экран искал корейское название отдела по ключу ko, но поскольку фактическое корейское значение было сохранено по ключу en, оно отображалось как пустое значение.

3. Анализ первопричины

В ходе расследования мы сначала проверили, какой код языка backend использует для запроса названий отделов. Поскольку на стороне запроса значения извлекались по ko — коду корейского языка, мы пришли к выводу, что причиной пустого ответа API, скорее всего, были сохранённые данные, а не условие запроса.

Проследив путь создания отдела, мы обнаружили, что batch интеграции с больницей использовал общий метод регистрации при регистрации нового отдела как Stage. По умолчанию этот метод получает текущую локаль из Spring's LocaleContextHolder и использует её в качестве кода языка.

public Tenant registerTenant(TenantCdo tenantCdo) { 
    return registerTenant( 
        tenantCdo, 
        LocaleContextHolder.getLocale().getLanguage() 
   ); 
} 

В web-запросе локаль может быть определена через контекст запроса, но batch не является потоком, напрямую инициированным пользователем в браузере. Поэтому во время выполнения batch явная локаль пользовательского запроса отсутствовала, и LocaleContextHolder использовал локаль JVM по умолчанию. В этой среде локалью по умолчанию была en, поэтому корейское название отдела, предоставленное адаптером больницы, было сохранено под ключом en.

4. Улучшение пути регистрации нового отдела

Изменения в коде были сосредоточены на пути регистрации нового отдела как Stage в batch интеграции с больницей. Поскольку проблемные данные представляли собой данные переводов, создаваемые batch при создании нового отдела, мы решили явно передавать код языка при новой регистрации Stage, не внося значительных изменений в общую логику синхронизации отделов.

Существующий метод registerTenant(TenantCdo) был сохранён, чтобы его можно было и дальше использовать для web-запросов. Вместо этого мы добавили перегруженный метод, позволяющий напрямую передавать код языка для путей batch и синхронизации, где локаль запроса недоступна или ненадёжна.

public Tenant registerTenant(TenantCdo tenantCdo) { 
    return registerTenant( 
        tenantCdo, 
        LocaleContextHolder.getLocale().getLanguage() 
   ); 
} 
 
public Tenant registerTenant( 
    TenantCdo tenantCdo, 
    String languageCode 
) { 
    Tenant tenant = Tenant.fromCdo( 
        tenantCdo, 
        languageCode 
   ); 
 
    return tenant; 
} 

В фактическом пути создания нового Stage значение ko передавалось явно, поскольку название отдела, предоставленное адаптером больницы, было на корейском языке.

return (Stage) tenantLogic.registerTenant(stageCdo, "ko"); 

Это позволило сохранять данные вновь созданных в batch отделов под нужным кодом языка, не затрагивая существующий путь web-запросов. Вместо изменения каждого потока, включая существующие пути изменения названий Stage и другие пути обновления, мы добавили необходимую обработку только в точке новой регистрации, где возникла проблема.

5. SQL для исправления данных

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

После проверки DB мы обнаружили, что проблема не ограничивалась средой разработки. Она возникала одинаково в средах разработки, staging и production, причём те же значения были отражены не только в исходной таблице, но и в отдельных данных для запросов, настроенных для повышения производительности запросов.

SQL для исправления данных был написан так, чтобы свести к минимуму его влияние на данные production. Мы сочли опасным просто изменить en на ko во всех записях, где language_code = 'en', поэтому применили одновременно следующие условия.

  • Изменяются только элементы, соответствующие данным отделов.
  • Изменяются только корректные данные.
  • Изменяются только данные, в которых код языка сохранён как en.
  • Изменяются только данные, зарегистрированные системной учётной записью.
  • Исключаются записи, если для того же отдела уже существует перевод ko.

Фактический запрос для исправления был записан в следующем виде.

update cm_tenant_translation tt 
set language_code = 'ko', 
    modified_by = 'system-fix', 
    modified_on = now() 
from cm_tenant t 
where t.id = tt.tenant_id 
  and t.tenant_type = 'STAGE' 
  and tt.valid_yn = true 
  and tt.language_code = 'en' 
  and tt.registered_by = 'system' 
  and not exists ( 
    select 1 
    from cm_tenant_translation ko 
    where ko.tenant_id = tt.tenant_id 
      and ko.language_code = 'ko' 
      and ko.valid_yn = true 
  ); 

Эти условия снизили риск ошибочного изменения фактических данных на английском языке или данных, зарегистрированных непосредственно пользователями. В фактическом скрипте Flyway исправления применялись не только к исходной таблице cm_tenant_translation, но и к namei18n в qm_tenant_view и tenant_namei18n в qm_membership_view, которые используются для выполнения запросов.

update qm_tenant_view v 
set namei18n = (v.namei18n - 'en') 
    || jsonb_build_object('ko', v.namei18n ->> 'en'), 
    modified_on = now() 
where v.tenant_type = 'STAGE' 
  and v.namei18n ? 'en' 
  and not (v.namei18n ? 'ko'); 
 
update qm_membership_view m 
set tenant_namei18n = (m.tenant_namei18n - 'en') 
    || jsonb_build_object('ko', m.tenant_namei18n ->> 'en'), 
    modified_on = now() 
where m.tenant_type = 'STAGE' 
  and m.tenant_namei18n ? 'en' 
  and not (m.tenant_namei18n ? 'ko'); 

6. Применение и проверка

В среде разработки мы сначала исправили данные с помощью запроса, а затем проверили, корректно ли отображается на странице My Page информация о ролях, специфичных для отдела. На этом этапе мы проверяли не только значения в DB, но и корректное отображение названий отделов на фактическом экране. Поскольку проблема, с которой столкнулись пользователи, возникала на экране, мы решили, что итоговую проверку также следует выполнять с точки зрения экрана.

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

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

7. Извлечённые уроки и заключение

Эта работа ещё раз подтвердила, что batch-обработку необходимо проектировать с учётом предположений, отличающихся от предположений обычных пользовательских запросов. Локаль, пользователь и информация из заголовков, естественным образом присутствующие в web-запросах, могут быть недоступны в batch или заменяться значениями среды по умолчанию. Если язык данных, создаваемых batch, известен однозначно, безопаснее указать его в коде, а не полагаться на контекст запроса.

Кроме того, важную роль в исправлении данных сыграли условия, ограничивающие область изменений. Проверив registered_by, tenant_type, valid_yn и наличие существующих данных ko, мы смогли исправить только целевые данные, созданные системным batch.

Этот случай начался с небольшой проблемы: некоторые названия отделов возвращались как пустые значения на странице My Page. Однако, одновременно изучив экран, ответ API, структуру хранения в DB и среду выполнения batch, мы смогли установить фактическую причину и устранить проблему как в пути создания новых данных, так и в уже существующих данных.

Lynn

Site footer