Что такое шаблоны в sql
Перейти к содержимому

Что такое шаблоны в sql

  • автор:

SQL-шаблоны

В enKod доступны два типа SQL-шаблонов – для сегментации и для динамического контента. Шаблоны для сегментации позволяют провести максимально глубокую фильтрацию базы, используя любые данные о клиентах, которые можно хранить в таблицах. SQL для ДК обычно используется для подстановки в письма необходимых товаров или иной персональной информации. enKod поддерживает синтаксис PostgreSQL. Узнать больше о синтаксисе запросов и возможностях SQL вы можете в документации в свободном доступе.

SQL для сегментации

Шаблоны данного типа всегда возвращают в ответе список контактов. SQL для сегментации также могут использоваться в условиях сегментов.

Чтобы создать SQL шаблон для сегментации перейдите во вкладку enKodSQL шаблоныСоздатьSQL для сегментов

Введите название SQL шаблона, описание (если необходимо) и само тело запроса.

Увидеть список контактов, подходящих под условия, можно после нажатия кнопки «Выполнить запрос».

Параметры в шаблонах нужны для унификации каких-либо стандартных запросов и позволяют менять тело запроса прямо из создания сегмента.

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

Таблица с участниками будет выглядеть подобным образом

Создаем шаблон для сегментации с параметром

Использовать шаблон в сегменте можно следующим образом

Для корректной работы SQL-шаблона для сегментации тип столбца в ТД должен быть «емейл», а не текст или иной другой. В шаблоне должен быть выбран только один столбец.

SQL для динамического контента

Шаблоны этого типа возвращают поля из таблицы, подходящие под заданные условия. Используются в письмах для персонализации и триггерных механик.

Чтобы создать SQL шаблон для ДК перейдите во вкладку enKodSQL шаблоныСоздатьSQL для динамического контента

Введите название SQL шаблона, описание (если необходимо) и само тело запроса.

Увидеть список полей, подходящих под условия, можно после нажатия кнопки «Выполнить запрос».

В шаблонах для ДК также можно использовать параметры.

Просмотр SQL-шаблона при создании сегмента

Для удобства в сегменте можно просмотреть само тело SQL-шаблона. Для этого нажмите на соответствующую кнопку после выбора шаблона. Редактировать запрос из сегмента нельзя.

Ведение разработки БД. Шаблоны создания/изменения объектов MSSQL

image

При постоянной работе с MSSQL необходимо создавать различные объекты БД: таблицы, представления, триггеры и т.д.

В статье приведу шаблоны SQL запросов, которые помогут, и, возможно, стандартизируют подход создания кода на языке T-SQL.

Кроме этого, опишу о том, как я веду репозиторий БД в системе контроля версий.

Основные требования реализации SQL скриптов

  1. скрипт должен выполняться многократно не выдавая ошибок
  2. в скрипте должны быть предусмотрены операторы PRINT для удобства отладки
  3. выполнение скриптов должно логироваться в один общий файл
  4. скрипты должны выполняться через командную строку используя стандартный набор утилит (sqlcmd, bcp)
  5. создание и изменение каждого объекта БД хранится отдельным SQL файлом
  6. SQL скрипты (файлы) запускаются BAT файлом при каждом обновлении БД

Далее приводятся примеры SQL файлов и BAT файл для запуска этих SQL запросов.

Шаблоны T-SQL

Создание/изменение таблицы

--USE [DatabaseName] --GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO --проверяем на существование таблицы в БД if objectproperty (object_id ('dbo.TableName'), N'IsUserTable') IS NULL begin print N'Создание таблицы - dbo.TableName' create table dbo.TableName ( TableNameId uniqueidentifier default newid() not null, FieldName1 uniqueidentifier not null, FieldName2 varchar(20) not null, CONSTRAINT PK_TableName PRIMARY KEY (TableNameId), CONSTRAINT FK_TableName_FieldName1 FOREIGN KEY (FieldName1) REFERENCES dbo.ReferenceTableName (RefFieldName) ON UPDATE CASCADE, CONSTRAINT UQ_TableName_FieldName1_FieldName2 UNIQUE (FieldName1, FieldName2) ); end GO -- Добавить поле if not exists ( select * from INFORMATION_SCHEMA.COLUMNS where TABLE_SCHEMA = 'dbo' and TABLE_NAME = 'TableName' and COLUMN_NAME = 'FieldName' ) begin alter table dbo.TableName add FieldName varchar(500) print N'Добавлено поле FieldName в таблице dbo.TableName' end -- Создать FOREIGN KEY, если его не существует if not exists (select * from sys.foreign_keys where object_id = OBJECT_ID(N'dbo.FK_TableName_FieldName1') AND parent_object_id = OBJECT_ID(N'dbo.TableName')) ALTER TABLE dbo.TableName WITH CHECK ADD CONSTRAINT FK_TableName_FieldName1 FOREIGN KEY(FieldName1) REFERENCES dbo.ReferenceTableName (RefFieldName) GO IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'dbo.FK_TableName_FieldName1') AND parent_object_id = OBJECT_ID(N'dbo.TableName')) ALTER TABLE dbo.TableName CHECK CONSTRAINT FK_TableName_FieldName1 GO -- Создание уникального индекса, если он не существует if not exists (select * from information_schema.key_column_usage where CONSTRAINT_NAME='UQ_TableName_FieldName1_FieldName2') begin ALTER TABLE dbo.TableName ADD CONSTRAINT UQ_TableName_FieldName1_FieldName2 UNIQUE (FieldName1, FieldName2) end -- Создание DEFAULT ограничения, если он не существует if not exists (select * from sysconstraints where AND COL_NAME(id,colid) = 'FieldName2' AND OBJECTPROPERTY(constid, 'IsDefaultCnst') = 1) begin ALTER TABLE dbo.TableName ADD CONSTRAINT DF_TableName_FieldName2 DEFAULT ('DefaultValue') FOR FieldName2 end declare @SchemaName varchar(128) = 'dbo' declare @TableName varchar(128) = 'TableName' -- Создание описания таблицы IF NOT EXISTS (SELECT * FROM fn_listextendedproperty('MS_Description', 'SCHEMA', @SchemaName, 'TABLE', @TableName, default, default)) EXECUTE sp_addextendedproperty N'MS_Description', N'Описание таблицы', N'SCHEMA', @SchemaName, N'TABLE', @TableName -- Создание описания поля, если его не существует IF NOT EXISTS (SELECT * FROM fn_listextendedproperty ('MS_Description', 'schema', @SchemaName, 'table', @TableName, 'column', 'FieldName1')) EXECUTE sp_addextendedproperty N'MS_Description', N'Описание поля FieldName1', N'SCHEMA', @SchemaName, N'TABLE', @TableName, N'COLUMN', N'FieldName1' GO

Создание/изменение представления

--USE [DatabaseName] --GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO --проверяем на существование представления в БД if objectproperty (object_id ('dbo.vw_ViewName'), N'IsView') is null BEGIN PRINT 'CREATE VIEW - '+db_name()+'.dbo.vw_ViewName' EXECUTE('CREATE VIEW dbo.vw_ViewName AS SELECT 1/0 as ColumnName'); END GO PRINT 'ALTER VIEW - '+db_name()+'.dbo.vw_ViewName' GO alter view dbo.vw_ViewName as select FieldName1, FieldName2 from dbo.TableName

Создание/изменение процедуры

--USE [DatabaseName] --GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO if objectproperty (object_id ('dbo.usp_StoredProcedureName'), N'IsProcedure') is null BEGIN -- If procedure exists, we exclude script execution. PRINT 'CREATE PROCEDURE - '+db_name()+'.dbo.usp_StoredProcedureName' EXECUTE('CREATE PROCEDURE dbo.usp_StoredProcedureName as select 1/0'); END ELSE PRINT 'ALTER PROCEDURE - '+db_name()+'.dbo.usp_StoredProcedureName' GO alter procedure dbo.usp_StoredProcedureName as select null

Создание/изменение триггера

--USE [DatabaseName] --GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO --проверяем на существование триггера в БД if objectproperty (object_id ('dbo.TriggerName'), N'IsTrigger') is null BEGIN PRINT 'CREATE TRIGGER - '+db_name()+'.dbo.TriggerName' EXECUTE('CREATE TRIGGER dbo.TriggerName on dbo.TableName for insert as print 0'); END GO PRINT 'ALTER TRIGGER - '+db_name()+'.dbo.TriggerName' GO alter trigger dbo.TriggerName on dbo.TableName for insert, update, delete as if (exists(SELECT 1 from inserted) and exists (SELECT 1 from deleted)) -- определение UPDATE begin if update(FieldName) begin update dbo.TableName set FieldName = FieldValue where SearchFieldName in ( select FieldValue from inserted union select FieldValue from deleted ) end end else if (exists(SELECT 1 from inserted) and not exists (SELECT 1 from deleted)) -- определение INSERT begin /* Ваш код обработки */ end else if (not exists(SELECT 1 from inserted) and exists (SELECT 1 from deleted)) -- определение DELETE begin /* Ваш код обработки */ end;

Создание/изменение табличного типа

--USE [DatabaseName] --GO /* DROP PROCEDURE dbo.ProcedureName DROP TYPE dbo.CustomUserType */ IF TYPE_ID(N'dbo.CustomUserType') IS NULL BEGIN PRINT N'Создание типа - dbo.CustomUserType' --Если тип не существует, создаем его. CREATE TYPE dbo.CustomUserType AS TABLE ( FieldId TYPE, PRIMARY KEY (FieldId) ) END GO

Создание/изменение скалярной функции

--USE [DatabaseName] --GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO if objectproperty (object_id ('dbo.ufn_FunctionName'), N'IsScalarFunction') is null BEGIN -- If procedure exists, we exclude script execution. PRINT 'CREATE FUNCTION - '+db_name()+'.dbo.ufn_FunctionName' EXECUTE('CREATE FUNCTION dbo.ufn_FunctionName() returns int begin return 0 end'); END ELSE PRINT 'ALTER FUNCTION - '+db_name()+'.dbo.ufn_FunctionName' GO alter function dbo.ufn_FunctionName () returns varchar(100) begin return null; end GO

Создание/изменение табличной функции

--USE [DatabaseName] --GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO if objectproperty (object_id ('dbo.ufn_TableFunctionName'), N'IsTableFunction') is null BEGIN -- If procedure exists, we exclude script execution. PRINT 'CREATE FUNCTION - '+db_name()+'.dbo.ufn_TableFunctionName' EXECUTE('CREATE FUNCTION dbo.ufn_TableFunctionName() returns table as return (select null as c)'); END ELSE PRINT 'ALTER FUNCTION - '+db_name()+'.dbo.ufn_TableFunctionName' GO ALTER function dbo.ufn_TableFunctionName() returns table as return ( select 1 as Field1, 2 as Field2 )

Удаление/создание XSD-схем (XmlSchemaCollection)

--USE [DatabaseName] --GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO if exists (select * from sys.xml_schema_collections where [schema_id] = schema_id('dbo') and [name] = 'SdmxXsd') begin drop XML SCHEMA COLLECTION dbo.SdmxXsd print 'Удалена XSD схема - dbo.SdmxXsd' end --Создать XSD схему, если она отсутствует в БД if not exists (select * from sys.xml_schema_collections where [schema_id] = schema_id('dbo') and [name] = 'SdmxXsd') begin CREATE XML SCHEMA COLLECTION dbo.SdmxXsd as N'         ' ALTER XML SCHEMA COLLECTION dbo.SdmxXsd add N'          ' ALTER XML SCHEMA COLLECTION dbo.SdmxXsd add N'                                             ' ALTER XML SCHEMA COLLECTION dbo.SdmxXsd add N'                                      ' print 'Создана XSD схема - dbo.SdmxXsd' end go

Ведение репозитория

1) Репозиторий состоит из папок, разделенных по типу объектов (например: Tables, Views, Triggers и т.д.)
2) Каждый объект БД хранится в отдельном SQL файле для удобства просмотра истории изменений в системе контроля версий.
3) Если изменения касаются данных, то такие изменения ведутся в отдельном файле «CommonChanges (version 000).sql», который создается на каждое обновление БД
4) Для автоматизации применения изменений на нескольких серверах запросы выполняются с помощью BAT файла.

Пример BAT файла

:: Описание: Обновление БД Microsoft SQL Server :: :: ВНИМАНИЕ: В случае если в разделе "1. Описание параметров" содержатся русские символы, то необходимо раскомментировать следующую строку ::chcp 1251 :: :: 1. ОПИСАНИЕ ПАРАМЕТРОВ :: :: Получить общие настройки (соединение с БД и др.) call Settings.bat :: :: Лог-файл выполнения запроса (перезаписывается с каждым вызовом sqlcmd) set LogFileName="%LogDir%\update_0001_%NowDateTime%_tmp.log" :: :: Полный лог-файл обновления БД set FullLogFileName="%LogDir%\update_0001_%NowDateTime%.log" :: :: Лог файла командного выполнения set LogCmdFileName="%LogDir%\update_0001_%NowDateTime%_cmd.log" :: :: 2. ВЫПОЛНЕНИЕ SQL-ЗАПРОСОВ :: set SqlFileName="..\CommonChanges (version 001).sql" @If Exist %SqlFileName% ( %sqlcmd% -S %ServerName% -U %UserName% -P %Password% -d %DatabaseName% -i %SqlFileName% -o %LogFileName% -b type %LogFileName% >> %FullLogFileName% ) ElSE ( echo Не найден файл скрипта %SqlFileName% >> %LogCmdFileName% ) :: set SqlFileName="..\AlterObjects\StoredProcedures\dbo.SP.sql" @If Exist %SqlFileName% ( %sqlcmd% -S %ServerName% -U %UserName% -P %Password% -d %DatabaseName% -i %SqlFileName% -o %LogFileName% -b type %LogFileName% >> %FullLogFileName% ) ElSE ( echo Не найден файл скрипта %SqlFileName% >> %LogCmdFileName% ) :: set SqlFileName="..\UpdateVersion.sql" @If Exist %SqlFileName% ( %sqlcmd% -S %ServerName% -U %UserName% -P %Password% -d %DatabaseName% -i %SqlFileName% -o %LogFileName% -b type %LogFileName% >> %FullLogFileName% ) ElSE ( echo Не найден файл скрипта %SqlFileName% >> %LogCmdFileName% ) :: :: Вывести содержимое лога файла командного выполнения в общий лог, если он был сформирован @If Exist %LogCmdFileName% ( echo ----Внимание, не найдены следующие файлы ожидающие выполнения:---- >> %FullLogFileName% type %LogCmdFileName% >> %FullLogFileName% :: Удалить лог файла командного выполнения del %LogCmdFileName% ) :: Удалить сокращенный лог (оставшийся при последнем вызове sqlcmd) del %LogFileName% :: :: Вывести содержимое файла-лога с результатом в командную строку type %FullLogFileName% pause

Файл Settings.bat

:: Настройки соединения с БД :: :: ВНИМАНИЕ: В случае если в разделе "1. Описание параметров" содержатся русские символы, то необходимо раскомментировать следующую строку ::chcp 1251 :: :: 1. ОПИСАНИЕ ПАРАМЕТРОВ :: :: Путь к файлу sqlcmd.exe на локальном диске set sqlcmd="sqlcmd.exe" :: :: Путь к файлу bcp.exe на локальном диске set bcp="bcp.exe" :: :: Имя SQL-сервера (именованный экземпляр) set ServerName="(local)\InstanceName" :: :: Имя пользователя set UserName="sa" :: :: Пароль set Password="my_password" :: :: Имя БД set DatabaseName=my_db :: :: Каталог логов set LogDir=.\log :: :: Использовать на русскоязычной версии Windows. Текущее время в формате YYYYMMDD_HHMMSS (рекомендуется использовать в имени файла лога) set NowDateTime=%date:~6%%date:~3,2%%date:~0,2%_%TIME:~0,2%%TIME:~3,2%%TIME:~6,2% :: Использовать на английской версии Windows. Текущее время в формате YYYYMMDD_HHMMSS (рекомендуется использовать в имени файла лога) ::set NowDateTime=%date:~10,4%%date:~4,2%%date:~7,2%_%TIME:~0,2%%TIME:~3,2%%TIME:~6,2% :: :: 2. СОЗДАНИЕ КАТАЛОГА ДЛЯ ХРАНЕНИЯ ЛОГОВ md %LogDir%

Достоинства

1) Удобно отслеживать историю изменения каждого объекта БД
2) Не требуется разрабатывать собственные приложения для выполнения SQL файлов
3) Работает на всех версиях MSSQL и Windows
4) Настройки соединения с БД, имена БД и др. переменные хранятся в одном файле «Settings.bat», которые легко изменить

Недостатки

1) Не предусмотрен общий откат изменений, если какой-то скрипт выполнится с ошибкой
2) Если репозиторий состоит из большого количества объектов БД, то чтобы не передавать заказчику весь репозиторий, нужно копировать файлы (входящие в обновление) в отдельный каталог

Выгрузка структуры БД

В качестве автоматизации выгрузки структуры БД, в формате один объект = один файл, на данный момент использую SSMS, но в скором времени подобный функционал будет в моей программе ImportExportDataSql, которую рекомендую всем разработчикам БД.

Немного о ImportExportDataSql

Приложение ImportExportDataSql бесплатное, без рекламы, оповещает о новых версиях, наличие командной строки и Вы можете скачать его и использовать в своих проектах.

С помощью ImportExportDataSql Вы сможете:

  • быстро загружать CSV файлы большого объема (более 1Гб) в SQL Server
  • загружать Excel файлы и CSV с возможностью настройки полей, а также с ограничением количества обрабатываемых строк (удобно при отладке)
  • выгружать выборочные данные из БД, в SQL формате и затем выполнять этот скрипт на другой БД (т.е. использовать как средство синхронизации данных)
  • копировать джобы с одной машины на другую

image

  • выгружать структуру БД.
  • Главной особенностью ImportExportDataSql, является то, что можно объединять несколько SELECT запросов, выгружая результат в виде SQL в один файл.

    Добавляйтесь в группу VK, пишите свои пожелания, буду рад доработать приложение под Ваши нужды.

    Заключение

    Рад был поделиться результатом своего опыта. Данный способ ведения репозитория меня вполне устраивает. Надеюсь, эта статья поможет нам навести порядок в базах данных.

    Поиск по шаблону

    В PostgreSQL есть несколько средств поиска текста по шаблону. Наиболее часто используемый оператор LIKE языка SQL :

    строка LIKE шаблон 

    В шаблоне LIKE можно использовать два спецсимвола

    • _ — заменяет один любой символ;
    • % — заменяет любую последовательность символов (в том числе пустую)
    'abc' LIKE 'abc' true 'abc' LIKE 'a%' true 'abc' LIKE '_b_' true 'abc' LIKE 'c' false 

    При проверке по шаблону LIKE всегда рассматривается вся строка. Поэтому, если нужно найти последовательность символов где-то в середине строки, шаблон должен начинаться и заканчиваться знаком % .

    Чтобы найти в строке буквальное вхождение знака процента или подчёркивания, перед соответствующим символом в шаблоне нужно добавить спецсимвол. По умолчанию в качестве спецсимвола выбрана обратная косая черта \ , но с помощью предложения ESCAPE можно выбрать и другой. Чтобы включить спецсимвол в шаблон поиска, продублируйте его. Синтаксис LIKE с указанием спецсимвола:

    строка LIKE шаблон ESCAPE спецсимвол 
    '_asdfa' LIKE '$_asd%' ESCAPE '$' 

    Вместо LIKE можно использовать ключевое слово ILIKE , чтобы поиск был регистр-независимым с учётом текущей языковой среды.

    Шаблоны в SQL Server Management Studio

    sql110

    Допустим нам нужно создать таблицу, т.е. обычный Create Table. Мы можем писать весь текст вручную, мы можем скопировать уже готовый скрипт и его адаптировать. Но в этом примере мы всё сделаем с помощью шаблонов.

    Короче кликаем по нужному шаблону (в примере “Create Table”, после чего появляется новый запрос и в нём текст запроса. У меня получился следующий:

    -- ========================================= -- Create table template -- ========================================= USE GO IF OBJECT_ID('.', 'U') IS NOT NULL DROP TABLE . GO CREATE TABLE . (  ,  ,  , CONSTRAINT PRIMARY KEY () ) GO

    Здесь хочется отметить, что параметр заключается в угловые скобки и состоит из 3-х частей, разделённых запятыми. Например для следующего параметра:

    schema_name – имя параметра в шаблоне

    sysname – тип параметра (int, char(20) и т.п., а также sysname)

    dbo – значение по умолчанию

    В выше приведённом шаблоне тип создаваемого столбца также задаётся отдельным параметром, поэтому при объявлении самого столбца тип не задаётся:

    Но если бы мы точно знали, что столбец c1 должен быть типа int, то могли бы записать так:

    Вернёмся к примеру.

    Осталось только параметры шаблона задать. Для этого в главном меню -> Query -> Specify Values for Template Parameters…

    sql112

    Ну или как можно заметить можно нажать Ctrl+Shift+M

    Появляется окошко, где мы можем настраивать нужные параметры:

    sql113

    Пробежались, задали значения:

    sql114

    Жмём ОК, и наш запрос принимает нормальный вид:

    -- ========================================= -- Create table template -- ========================================= USE BestDatabase GO IF OBJECT_ID('dbo.ForDelete', 'U') IS NOT NULL DROP TABLE dbo.ForDelete GO CREATE TABLE dbo.ForDelete ( id int NOT NULL, name char(20) NULL, kode char(20) NULL, CONSTRAINT PK_ForDelete PRIMARY KEY (id) ) GO

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

    Пользовательские шаблоны

    А теперь давайте создадим свой шаблон для создания справочника, чтобы в будущем нам нужно было делать минимум операций.

    Для этого в нужной папке браузера шаблонов создаём новый.

    Примечание: Вероятно есть смысл в создании специальной папки для пользовательских шаблонов, но я в примере новый шаблон добавил в папку Table

    sql115

    Теперь внесём в него необходимый код. Для этого в контекстном меню этого шаблона нужно выбрать Edit. Ведь двойной клик – это уже использование шаблона.

    sql116

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

    -- ========================================= -- Создание таблицы справочника -- ========================================= USE GO IF OBJECT_ID('.', 'U') IS NOT NULL DROP TABLE . GO CREATE TABLE . ( id int NOT NULL, name char(20) NULL, code char(20) NULL, CONSTRAINT PK_ PRIMARY KEY (id) ) GO

    Можно сохранять шаблон.

    Использование пользовательского шаблона

    Как и раньше двойной клик на имени шаблона, открывается новый запрос заполненный текстом шаблона. Заходим в настройку параметров шаблона (Ctrl+Shift+M)

    sql117

    Можно заметить, что указать нужно 3 параметра, но если имя схемы в проекта будет всегда dbo, то можно и его убрать из параметров.

    После чего будет сформирован следующий скрипт:

    -- ========================================= -- Создание таблицы справочника -- ========================================= USE TSQL2012 GO IF OBJECT_ID('dbo.NewDictionary', 'U') IS NOT NULL DROP TABLE dbo.NewDictionary GO CREATE TABLE dbo.NewDictionary ( id int NOT NULL, name char(20) NULL, code char(20) NULL, CONSTRAINT PK_NewDictionary PRIMARY KEY (id) ) GO

    Выполняем его. Таблица успешно создаётся:

    sql118

    Written by Igor Motskin

    Периодически мы проводим обучение и даем возможность стажировки на базе нашей платформы с управлением на SQL. Если вам это интересно, то пожалуйста посмотрите информацию об обучении/стажировке по SQL.

    Опубликовано в Разное

    • Демонстрация компонентов Falcon Space
    • Смотреть демо веб-платформы Falcon Space
    • Подпишись на наш видеоканал в Youtube

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *