Как изменить значение в JSON прочитав значение из другого узла с помощью SQL запроса

В предыдущей статье Как добавить узел в JSON с помощью SQL запроса было рассмотрено, как воспользовавшись средствами самого SQL вставить новый узел в сериализованный JSON объект, хранящийся в ячейке БД. В данной статье рассмотрим, как изменить значение по ключу в JSON объекте, предварительно считав его из другого поля JSON объекта.

Как это можно сделать в PostgreSQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
START TRANSACTION;

DO $$
DECLARE json_data JSONB;
DECLARE len INT;
DECLARE path text[];
DECLARE base text;
DECLARE filter text;
BEGIN
-- Читаем существующие JSON данные из поля Configuration
SELECT "Configuration" INTO json_data FROM "IdentityIntegration" WHERE "Priority" = 1;

-- Вытаскиваем длину узла Servers, который представлет из себя массив json объектов
SELECT jsonb_array_length(json_data->'Servers') into len;

-- Для каждого элемента массива Servers
FOR i IN 0..len-1 LOOP
-- формируем путь к ключу
path := ARRAY['Servers', i::text, 'IsReferral'];
-- устанавливаем значение json_data по ключу path в false
json_data = jsonb_set(json_data, path, 'false'::jsonb, true);

-- считываем значения из json объекта json_data в переменные base и filter
SELECT json_data->'Servers'->i->'Sources'->0->'Base' into base;
SELECT json_data->'Servers'->i->'Sources'->0->'Filter' into filter;

-- формируем путь к ключу
path := ARRAY['Servers', i::text, 'GroupSource'];

--RAISE NOTICE 'Iteration: %', ('{"Base": ' || base || ', "Filter": ' || filter || '}'); -- печатаем в консоль если требуется

-- устанавливаем значение json_data по ключу path в сложный объект, получающийся конкатенацией переменных base и filter
json_data = jsonb_set(json_data, path, ('{"Base": ' || base || ', "Filter": ' || filter || '}')::jsonb, true);
END LOOP;

-- Обновляем поле Configuration изменённым значением JSON данных
UPDATE "IdentityIntegration" SET "Configuration" = json_data WHERE "Priority" = 1;
END $$;

COMMIT;

Последний параметр со значения true в функции jsonb_set именуется create_if_missing и является опциональным.

Как это можно сделать в MSSQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
BEGIN TRANSACTION;
GO

DECLARE @json_data NVARCHAR(MAX);
DECLARE @len INT;
DECLARE @path NVARCHAR(MAX);
DECLARE @base NVARCHAR(MAX);
DECLARE @filter NVARCHAR(MAX);

SET @json_data = (SELECT [Configuration] FROM [IdentityIntegration] WHERE [Priority] = 1);

SET @len = (SELECT COUNT(*) FROM OPENJSON(JSON_QUERY(@json_data, '$.Servers')));
-- PRINT @len; -- печатаем в консоль если требуется

DECLARE @i INT;
SET @i = 0;

WHILE @i < @len
BEGIN
SET @path = CONCAT('$.Servers[', @i, '].IsReferral');
SET @json_data = JSON_MODIFY(@json_data, @path, 'false');

SET @base = JSON_VALUE(@json_data, CONCAT('$.Servers[', @i, '].Sources[0].Base'));
SET @filter = JSON_VALUE(@json_data, CONCAT('$.Servers[', @i, '].Sources[0].Filter'));

SET @path = CONCAT('$.Servers[', @i, '].GroupSource');
-- применение функции JSON_QUERY(...) к содержимому изменяемого узла удаляет избыточно экранированные кавычки
SET @json_data = JSON_MODIFY(@json_data, @path, JSON_QUERY(CONCAT('{"Base": "', @base, '", "Filter": "', @filter, '"}')));

SET @i = @i + 1;
END;

UPDATE [IdentityIntegration] SET [Configuration] = @json_data WHERE [Priority] = 1;

COMMIT;
GO

Обращаю внимание на необходимость применения функции JSON_QUERY(...) к содержимому изменяемого узла, иначе вставляемые кавычки будут избыточно экранированы.

Как добавить узел в JSON с помощью SQL запроса

Рассмотрим ситуацию, когда в Базе Данных (БД) имеется сохранённый в виде строки сериализованный JSON объект, а нам требуется добавить в него новый узел. Конечно, можно взять язык программирования, считать значение из БД, изменить его и перезаписать в БД. Но хочется воспользоваться средствами самого SQL для подобной операции.

Как это можно сделать в MSSQL:

1
2
3
4
5
6
7
8
9
10
11
12
DECLARE @json NVARCHAR(2000);

-- Читаем существующие JSON данные из поля Configuration
SET @json = (SELECT [Configuration] FROM [dbo].[Integration] WHERE [Priority] = 1);

-- Изменим JSON данные добавив в них новый узел Cache
SET @json = JSON_MODIFY(@json, '$.Cache', JSON_QUERY('{"Interval":1,"IntervalDimension":"day"}'));

-- Обновим поле Configuration изменённым значением JSON данных
UPDATE [dbo].[Integration]
SET [Configuration] = @json
WHERE [Priority] = 1;

Обращаю внимание на необходимость применения функции JSON_QUERY(...) к содержимому добавляемого узла, иначе вставляемые кавычки будут избыточно экранированы.

Как это можно сделать в PostgreSQL:

1
2
3
4
5
6
7
DO $$
DECLARE json_data JSONB;
BEGIN
SELECT "Configuration" INTO json_data FROM "Integration" WHERE "Priority" = 1;
json_data = jsonb_set(json_data, '{Cache}', '{"Interval": 1, "IntervalDimension": "day"}', true);
UPDATE "Integration" SET "Configuration" = json_data WHERE "Priority" = 1;
END $$;

Последний параметр со значения true в функции jsonb_set именуется create_if_missing и является опциональным.

В случае удаления ненужного узла из JSON объекта требуется установить значение узла в NULL, как показано ниже для MSSQL:

1
2
3
4
5
6
7
8
9
DECLARE @json NVARCHAR(2000);

SET @json = (SELECT [Configuration] FROM [dbo].[Integration] WHERE [Priority] = 1);

SET @json = JSON_MODIFY(@json, '$.Cache', NULL);

UPDATE [dbo].[Integration]
SET [Configuration] = @json
WHERE [Priority] = 1;

System.Drawing.Common будет поддерживаться только на Windows

Начиная с версии .NET6 и выше Microsoft приостанавливает поддержку библиотеки System.Drawing.Common на других платформах, кроме Windows. Это связано с рядом проблем кроссплатформенного портирования GDI+ на другие операционные системы (ОС). В вышеприведённой статье описаны альтернативы System.Drawing.Common, но для версии .NET6 ещё можно продолжать использовать классы из System.Drawing.Common в отличных от Windows ОС.

Для этого:

  1. В проекте, где используются классы из библиотеки System.Drawing.Common, в файле проекта с расширением .csproj (обычно доступны по нажатию клавишы F4 из IDE) требуется добавить такую секцию:
1
2
3
4
5
...
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />
</ItemGroup>
...

Это приведёт к тому, что при сборке проекта в файле вида [appname].runtimeconfig.json окажется секция:

1
2
3
4
5
6
7
{
"runtimeOptions": {
"configProperties": {
"System.Drawing.EnableUnixSupport": true
}
}
}
  1. Требуется убедиться, что на ОС установлен пакет libgdiplus. Если пакет отсутствует, то его нужно установить.
    Для Linux это можно сделать так: sudo apt install libgdiplus

Необходимо учесть, что начиная с .NET7 этот трюк больше не сработает, о чем предупреждается в вышеприведённой статье о Breaking Change от Microsoft.

Очистка кэша NuGet пакетов

Давеча выяснил, что мой жёсткий диск забит 7,7 Гигабайтами nuget пакетов. Показалось это перебором и я решил почистить кеш.

Самый простой способ сделать это - с помощью Visual Studio. Для этого перехожу в меню: Tools → Options → NuGet Package Manager → General и нажимаю кнопку Clear All NuGet Storage.

Аналогичную операцию можно выполнить из командной строки, правда для этого понадобится NuGet Commandline. После скачивания перехожу в папку, где расположен nuget.exe .

Чтобы увидеть локальные кэши выполняю команду:

1
nuget locals all -list

Чтобы очистить локальные кэши выполняю команду:

1
nuget locals all -clear

Вот вывод терминала касательно моих кэшей на рабочей машине и процесса их очистки:

Затем повторно оцениваю содержимое категории Other в операционной системе:

Результатом удовлетворён. Заодно почистил неактуальные сырцы (source) и кэш vscode.

Об одной проблеме работы ngx-translate при первоначальной загрузке сайта на Angular

При исследовании аудитории пользователей моего проекта для изучения таблицы умножения УмноДел выяснилось, что кроме русскоязычных пользователей, заходит немалое количество пользователей из других стран. Это подтолкнуло меня на поиски быстрого способа добавления переключателя языков (RU, EN) в моё Angular приложение. Очень большое подспорье оказала статья How to translate your Angular app with ngx-translate, только с BabelEdit мне подружиться не удалось и многие пункты из статьи я просто выполнил вручную. В итоге всё успешно получилось и без него.

Только одна заковыка меня поджидала с элементом <mat-select>. При первоначальной загрузке сайта его значение по умолчанию не отображалось и создавалось впечатление, что значение не установлено, хотя это не так.

В конструкторе app.component.ts значение было установлено selectedOperations = 'multiply'; , а при клике по элементу видно, что этот пункт уже выбран. При последующих переключениях языка select сразу устанавливался правильно и я начал подозревать, что проблема связана с корректной подгрузкой перевода ngx-translate при первоначальной загрузке. Какое-либо действие с элементом, либо переключение языков приводило сразу же к корректному отображению значения элемента!

Стоит отметить, что ngx-translate динамически подгружает переводы значений и в некоторых случаях могут вместо значений переводов отображаться ключи словарей, пока эти словари подгружаются. Это именуется глюками от использования TranslateLoader (glitches when using TranslateLoader) и решение в борьбе с ними заключается в предварительной подгрузке переводов из json-файла при первоначальной загрузке приложения. Я использовал аналогичное решение, чтобы подгрузить значения предварительно, а не только динамически.

Для этого в требуется включить возможность загрузки json-файлов в tsconfig.app.json:

1
2
3
4
5
6
"compilerOptions": {
...
"resolveJsonModule": true,
"esModuleInterop": true
...
},

Затем добавить в файл app.component.ts загрузку дефолтного значения языка из json-файла:

1
import defaultLanguage from "./../assets/i18n/en.json";

И наконец установить дефолтное значение перевода из json-файла в конструкторе:

1
2
3
constructor(private bllService: BllService, private translate: TranslateService) {
translate.setTranslation('ru', defaultLanguage);
translate.setDefaultLang('ru');

Как результат увидим ожидаемое поведение элемента <mat-select> с предзаполненым значением по умолчанию:

Метод Contains больше не поддерживается в EF Core 6

При переходе с .NET 5 на .NET 6 столкнулся с такой проблемой: EF Core отказался строить сложные IQueryable запросы LINQ в .NET 6, с которыми ранее справлялся в .NET 5 без проблем. Поиски ответа на вопрос, что же случилось, привели к следующему решению.

При использовании .NET 5 и ниже использование метода Contains для проверки содержится ли значение в множестве других значений препокойно транслировалось в SQL выражение вида WHERE City IN ('Paris','London'), но начиная с версии .NET 6 метод Contains не может как раньше транслироваться в SQL и падает с исключением, предлагающим упростить выражение или переписать его через другие операторы.

Таким образом, такой код больше не работает в .NET 6:

1
2
var ids = new List<int> { 1, 2, 3 };
var entities = dbContext.Posts.Where(p => ids.Contains(p.Id));

Решение заключается в использовании метода Any вместо Contains для проверки содержится ли значение в множестве других значений:

1
2
var ids = new List<int> { 1, 2, 3 };
var entities = dbContext.Posts.Where(p => ids.Any(id => id == p.Id));

Вот такое LINQ выражение .NET 6 уже принимает спокойно и EF Core транслирует в ту же самую конструкцию SQL вида WHERE Id IN (1,2,3).

Переписав все конструкции содержащие ids.Contains в ids.Any всё стало работать как и прежде, и задача была успешно решена.

Хотел бы отметить, что данная проблема касается только выборки из множества значений. String.Contains данная проблема не коснулась и он нормально транслируется в конструкцию SQL вида LIKE.

Обязательный QueryString параметр в ASP.NET Core

У меня возникла потребность для HttpGet запроса проверять, что в качестве QueryString параметра запроса прислан не просто какой-то там string, а что присланое значение успешно преобразуется к перечислению Enum вида:

1
2
3
4
5
public enum QrCodeTypeEnum
{
Location,
Device
}

Причём если QueryString параметр “левый”, или вообще не прислан, то не должно самопроизвольно выбираться дефолтное значение перечисления QrCodeTypeEnum.Location как при обычном [FromQuery] аттрибуте (тут даже аттрибут [BindRequired] не поможет).

Для решения данной задачи потребовалось расширить возможности FromQueryAttribute и реализовать интерфейс IParameterModelConvention, который обязует имплементировать метод void Apply(ParameterModel parameter).

Вот как я это сделал:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

// ReSharper disable once CheckNamespace
namespace Microsoft.AspNetCore.Mvc
{
public class RequiredFromQueryAttribute : FromQueryAttribute, IParameterModelConvention
{
public void Apply(ParameterModel parameter)
{
if (parameter.Action.Selectors != null && parameter.Action.Selectors.Any())
{
Func<string, bool> rejectionPredicate = null;

if (parameter.ParameterType.IsEnum)
{
var array = Enum.GetValues(parameter.ParameterType);
List<string> values = null;
foreach (var paramValue in array)
{
values ??= new List<string>();
values.Add(paramValue.ToString());
}

if (values is { Count: > 0 })
rejectionPredicate = x => !values.Contains(x, StringComparer.InvariantCultureIgnoreCase);
}

parameter.Action.Selectors.Last().ActionConstraints.Add(
new RequiredFromQueryActionConstraint(
parameter.BindingInfo?.BinderModelName ?? parameter.ParameterName, rejectionPredicate
)
);
}
}
}
}

При инициализации контроллера, содержащего наш атрибут [RequiredFromQuery], происходит вызов метода Apply, который понимает, что аттрибут применён к перечислению, благодаря свойству parameter.ParameterType.IsEnum, и формирует предикат, который передаётся конструктору RequiredFromQueryActionConstraint и будет использован при поступлении запроса в контроллер для проверки поступающего значения в QueryString.

Давайте взглянем как реализован класс RequiredFromQueryActionConstraint, который имплементирует интерфейс IActionConstraint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System;
using Microsoft.AspNetCore.Mvc.ActionConstraints;

// ReSharper disable once CheckNamespace
namespace Microsoft.AspNetCore.Mvc
{
public class RequiredFromQueryActionConstraint : IActionConstraint
{
private readonly string _parameterKey;
private readonly Func<string, bool> _rejectionPredicate;

public RequiredFromQueryActionConstraint(string parameterKey, Func<string, bool> rejectionPredicate)
{
_parameterKey = parameterKey;
_rejectionPredicate = rejectionPredicate;
}

public int Order => 999;

public bool Accept(ActionConstraintContext context)
{
if (!context.RouteContext.HttpContext.Request.Query.ContainsKey(_parameterKey))
{
return false;
}

if (!context.RouteContext.HttpContext.Request.Query.TryGetValue(_parameterKey, out var value))
{
return false;
}

if (_rejectionPredicate != null && _rejectionPredicate.Invoke(value.ToString()))
{
return false;
}

return true;
}
}
}

При поступлении HttpGet запроса вида /v3/device/download-report?type=device к контроллеру device, вызывается метод Accept класса RequiredFromQueryActionConstraint , который проверяет наличие ключа type, наличие значения у этого ключа, и что значение ключа соответствует предикату _rejectionPredicate, который формируется из значений перечисления в методе Apply атрибута RequiredFromQueryAttribute. Теперь проверка QueryString параметра запроса осуществляется строго, и в случае отсутствия ключа type, либо неверного (отсутствующего в перечислении) значения ключа type методом будет дан ответ с кодом 400 и сообщением вида:

1
2
3
4
5
{
"id": [
"The value 'download-report' is not valid."
]
}

Использовать атрибут [RequiredFromQuery] можно в методе контроллера следующим образом:

1
2
3
4
5
6
[HttpGet("download-report")]
[Authorize(Policy = PolicyNameProvider.AuthenticatedIdentityIsCurrentAccountAdmin)]
public FileStreamResult DownloadAccountQrCodesReport([RequiredFromQuery] QrCodeTypeEnum type)
{
var qrCodeType = type.ToQrCodeType();
... и так далее

При решении данной задачи я опирался на статью Required query string parameters in ASP.NET Core MVC, где содержится намного больше деталей описанного выше процесса.

Как восстановить базу данных из файла бэкапа в PostgreSQL

Для восстановления базы данных из файла бэкапа в PostgreSQL есть несколько методов, но если размер файла с “дампом” очень велик, то восстановить базу можно только из консоли.

Для этого открываем PowerShell от имени администратора и выполняем следующую команду вида:

1
psql -U username -d database_name -f backup.sql

, где username - имя пользователя базы данных, database_name - имя базы данных, backup.sql - имя файла с бэкапом базы данных.

Например:

1
psql -U postgreadmin -d smp_tech -f "D:\\smp_tech_20221208.sql"

, где postgreadmin - имя владельца базы данных smp_tech, а "D:\\smp_tech_20221208.sql" - путь к файлу с “дампом” базы.

После ввода этой команды консоль попросит пароль пользователя postgreadmin и восстановит базу данных.

Если PostgreSQL находится на удалённой машине, то в вышеприведённую строку требуется ещё добавить ключ -h:

1
psql -U postgreadmin -h 192.168.3.177 -d smp_tech -f "D:\\smp_tech_20221208.sql"

Как уменьшить размер файла логов в Microsoft SQL Server

При создании Базы Данных (БД) в MS SQL Server для оптимизации логирования БД требуется установить Recovery Mode в значение Simple.

Для уже созданной БД с режимом Recovery Mode = Full требуется перейти в свойства БД и в разделе Options перевести соответствующую настройку из значения Full:

в значение Simple:

Затем необходимо очистить лишние записи действий в файле логов, т.к. модель восстановления данных Recovery Mode изменилась.

Для этого выберите требующуюся БД, затем кликните по ней Правой Кнопкой Мыши (ПКМ) и выберите Tasks → Shrink → Files:

После этого в появившемся окне формы Shrink File требуется установить File type: Log и выбрать Release unused space

Текущий размер занимаемого дискового пространства логами и свободное место для них можно оценить с помощью параметров Currently allocated space и Available free space этой же формы Shrink File (при повторном её открытии можно увидеть размеры после применения операции сжатия файла логов)

Настройка в React/TypeScript приложении ESLint, Prettier и Husky

Попросили меня друзья помочь с настройкой CreateReactApp (CRA) приложения, чтобы синтаксис проверялся, красота кода в одном стиле автоматически наводилась, и чтобы не давало закоммитить, если всё вышесказанное не выполняется.

Начал я с настройки ESLint. Он нужен для проверки синтаксиса, стиля кодирования и поиска ошибок в коде. По умолчанию ESLint уже есть в CRA, его нужно только инициализировать через консоль в папке проекта:

1
yarn eslint --init

И затем настроить. Установите через консоль следующие плагины в папку проекта:

1
yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-airbnb-typescript eslint-plugin-jest eslint-config-airbnb

Заполните файл .eslintrc.json следующими настройками:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{
"env": {
"browser": true,
"es2022": true,
"jest": true
},
"extends": [
"airbnb-typescript",
"airbnb/hooks",
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:@typescript-eslint/recommended",
"plugin:jest/recommended",
"plugin:prettier/recommended",
"plugin:import/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": [ "react", "@typescript-eslint", "jest" ],
"rules": {
"no-alert": "warn",
"no-console": "warn",
"linebreak-style": "off",
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
},
{ "usePrettierrc": true }
],
"react/prop-types": "off",
"@typescript-eslint/no-unused-vars": ["warn"]
},
"settings": {
"react": {
"createClass": "createReactClass",
"pragma": "React",
"fragment": "Fragment",
"version": "detect",
"flowVersion": "0.53"
},
"propWrapperFunctions": [
"forbidExtraProps",
{"property": "freeze", "object": "Object"},
{"property": "myFavoriteWrapper"},
{"property": "forbidExtraProps", "exact": true}
],
"componentWrapperFunctions": [
"observer",
{"property": "styled"},
{"property": "observer", "object": "Mobx"},
{"property": "observer", "object": "<pragma>"}
],
"formComponents": [
"CustomForm",
{"name": "Form", "formAttribute": "endpoint"}
],
"linkComponents": [
"Hyperlink",
{"name": "Link", "linkAttribute": "to"}
]
}
}

Из конфига ESLint видно, что мы используем AirBnb, а также настраиваем сразу и Prettier. Prettier - это инструмент автоматического форматирования кода. Для его установки запустите в консоли:

1
yarn add -D prettier eslint-config-prettier eslint-plugin-prettier

Cоветую создать файл .eslintignore и поместить туда следующее содержимое:

1
2
3
4
5
6
build/*
public/*
docs/*
templates/*
src/react-app-env.d.ts
src/serviceWorker.ts

Создайте файл .prettierrc и поместите туда следующее содержимое:

1
2
3
4
5
6
7
{
"trailingComma": "es5",
"printWidth": 100,
"semi": false,
"singleQuote": true,
"tabWidth": 2
}

Осталось только поправить package.json, внеся туда следующие секции:

1
2
3
4
5
6
7
8
9
10
11
12
13
"scripts": {
.... // предыдущий полезный код скриптов
"format": "prettier --write src/**/*.{ts,tsx,scss,css,json}",
"lint": "tsc --noEmit && eslint src/**/*.ts{,x} --color",
"lint-staged": "lint-staged",
"prepare": "husky install"
}
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},

Теперь можно проверить что всё впорядке, запустив в консоли следующие команды:

1
2
yarn lint
yarn format

Если вы настроили всё правильно команды отработают без ошибок.

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

Для этого установим Husky, который будет реагировать на событие попытки коммита, следующей командой из консоли:

1
yarn add -D lint-staged husky

Для инициализации Husky выполните в консоли команду:

1
yarn husky install

Так как у нас версия Husky 7, то в соответствующей скрытой папке .husky необходимо внести следующие изменения в файл pre-commit:

1
2
3
4
5
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn lint
yarn format

Теперь вам удастся закоммитить ваш код только в случае успешного выполнения скриптов из файла pre-commit.