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

Читабельность

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

Форматирование

  1. Каждый логический блок (запрос) должен иметь отступ.
  2. Обязательные псевдонимы для колонок (об этом в конце) должны быть выровнены по одной вертикальной линии. Можно делать более одного отступа табуляцией.
  3. Необязательно ставить псевдоним в одну строку с полем. Для обеспечения п.2 можно вынести псевдоним в следующую строку и выровнять ее по остальным псевдонимам в данной выборке.
  4. Также, выравниваются сами поля выборки.
  5. У оператора JOIN операнд ON должен идти в следующей строке с отступом в 1 табуляцию. То есть считаем это подблоком.
  6. Вдобавок, операнды после знака равно должны быть выровнены по одной вертикальной линии, аналогично псевдонимам.
  7. У сложных операторов, таких как WHERE, каждый операнд выстраивается в новую строку с доп. отступом.
  8. Используйте скобки для выделения блоков. Скобки желательно выделять подобно if-блокам на C#.
  9. Встроенные ключевые слова на SQL всегда пишутся большими буквами.
  10. Подзапросы пишутся с отступом от родительского запроса, соблюдая правила предыдущих пунктов.

Пример кода со всеми вышеописанными пунктами приведен ниже.

-- псевдокод, может не скомпилироваться
-- 1.1 корневой блок, нулевой отступ
CREATE VIEW dbo.elements
AS
    -- 1.2 блок с запросом - один отступ
    SELECT  -- здесь лучше оставлять место для TOP N
        -- 1.3 блок с проекцией запроса - еще один отсуп
        
            table.id            AS id   
    -- однако запятая - без отступа - удобнее модифицировать
    ,       table.text          AS text
    -- подблоки имеют отступ
    ,       CASE WHEN table.id = 0
                THEN 'foo'
                ELSE 'bar'
            END                 AS foobar
    ,       category.text       AS category
    -- вернулись к блоку с запросом
    FROM    dbo.some_table      AS table -- не брезгуйте осмысленными псевдонимами
    JOIN    dbo.some_categories AS categories
    -- отступ!
        ON  table.categoryID    = categories.id
    -- скобки для блоков, выравнивание подблоков и подзапросов
    WHERE   (
            table.text <> 'invalid text'
        AND table.text <> 'other invalid text'
    )
    OR      table.categoryID = (
                                    SELECT  c.id
                                    FROM    dbo.categories AS c
                                    WHERE   c.type <> 'invalidType'
                                )

GO

Как видно, псевдонимы AS желательно выровнять для всех частей запроса.

Разбиение

С разбиением всё достаточно просто – при выборках не стоит забывать (ровно как и злоупотреблять) таким инструментом как Common Table Expression, а в хранимых процедурах можно делать подпроцедуры, курсоры, переменные, в общем всё что может дать процедурный язык и практика ведения чистого кода. Главное – помнить что в SQL это палка о двух концах. Например, CTE компилируются только один раз, а в выборке могут участвовать в самых разных ситуациях. Это сильно затрудняет работу оптимизатора и может кардинально ухудшить производительность.

Контракты

Принцип устройства процедур и представлений позволяет предоставить контракт для прикладного уровня на самой ранней стадии разработки – то есть когда реальной логики еще нет. Это позволяет единожды прописать правильное использование контракта на клиенте, и больше не возвращаться к этому вопросу, а пользоваться доступными объектами как есть. Поскольку данный процесс синхронизации контрактов может занимать длительное время, особенно если требуется изменить структуру БД – эта практика позволяет ускорить процесс разработки, попросту вычеркнув из нее этап постоянной синхронизации модели. Более того, предоставление доступа к БД подобным образом позитивно сказывается на её безопасности – у внешнего кода нет доступа к внутреннему состоянию базы, как для чтения так и для модификации.

То есть внешний код полностью абстрагируется от деталей реализации базы, полагаясь только на объявленный контракт.

Ниже продемонстрировано как добиться подобного эффекта на стадии начала разработки:

CREATE VIEW dbo.elements
AS
    SELECT  
            1                   AS id   
    ,       'foobar'            AS text
    ,       CASE WHEN 1 <> 1
                THEN NULL
                ELSE 1
            END                 AS nullableInt
    FROM    dbo.some_table      AS table 
    WHERE   1 <> 1 -- TODO: реализовать логику запроса
GO

CREATE PROCEDURE dbo.insertElement
        @text           nvarchar(80)
    --  системный контракт: пусть все хранимки на вставку возвращают айди
    ,   @resultID	    int OUTPUT
    --  системный контракт: пусть все хранимки возвращают текст ошибки
    ,   @errorCode      int OUTPUT
    ,   @errorMessage   nvarchar(255) OUTPUT
AS
-- TODO:
GO

Есть несколько ньюансов:

  • В представлении важно тщательно следить за форматом данных. Если по задумке требуется Nullable<int> – нужно тщательнее проследить за тем чтоб импортер модели построил требуемую модель, основываясь на возвращаемых данных. Например, для поля nullableInt используется трюк который заставляет строитель модели EntityFramework добавлять обнуляемое поле. Есть более краткие способы,  однако данный код точно работает.
  • Если хранимая процедура возвращает таблицу – она может не попасть под авто-контракт.
  • Входные параметры процедур можно подвергнуть универсализации. На стороне БД это обязательное наличие определенных, например, OUTPUT полей, а на прикладном уровне – абстрактный класс параметров от которого будут наследоваться более конкретные запросы.

Таким образом, можно составить контракт БД на ранней стадии разработки и согласовать его с внешним кодом.

Автотестирование

Благодаря процедурной природе СУБД, можно организовать SQL файл таким образом, чтоб сразу при компиляции объекта выполнялась как его замена, так и автоматическое тестирование.

Автоподмена делается с помощью DROP VIEW/PROCEDURE в каждом файле соответствующего объекта. Следует учесть что в данном примере предполагается что новая версия обратно совместимая с предыдущей. В противном случае требуется контроль версий объектов, но об этом в другой статье.

Автоматическое тестирование делается путем вызова самого объекта после его объявления. В случае с хранимыми процедурами, которые меняют состояние БД, желательно применять откатываемые транзакции. Остается вопрос – какие данные подставлять? На тестовом сервере это должны быть предопределенные идентификаторы – так проще. В противном случае можно писать бесконечно много доп. логики для теста.

Ниже показан пример для представления и хранимой процедуры:

-- авторегенерация объекта
IF OBJECT_ID('dbo.vw_elements', 'V') IS NOT NULL
    DROP VIEW dbo.vw_elements;
GO

CREATE VIEW dbo.elements
AS
    SELECT  
            1                   AS id  
    FROM    dbo.some_table      AS table 
GO

-- test
SELECT  * FROM dbo.elements

 

-- авторегенерация объекта
IF OBJECT_ID('dbo.sp_psl_addServiceLocalizedText', 'P') IS NOT NULL
    DROP PROCEDURE dbo.sp_psl_addServiceLocalizedText;
GO

CREATE PROCEDURE dbo.sp_psl_addServiceLocalizedText
	@itemID		int,
	@langID		int,
	@itemType	nvarchar(64),
	@text		nvarchar(2000),
	@resultID	int		OUTPUT
AS
	INSERT INTO dbo.psl_languageLookupTexts 
				(itemID, langID, itemType, text)
	VALUES		(@itemID, @langID, @itemType, @text);
	SET @resultID = SCOPE_IDENTITY();

RETURN
GO

-- test
BEGIN TRANSACTION
    DECLARE @testText	nvarchar(2000);
    DECLARE @testType	nvarchar(64);
    DECLARE @testResult int;

    SET @testText = 'dbo.sp_psl_addServiceLocalizedText';
    SET @testType = 'psl_text_localize_type';


    DELETE FROM dbo.psl_languageLookupTexts WHERE text = @testText;

    EXECUTE dbo.sp_psl_addServiceLocalizedText 
            @itemID		= 1,
            @langID		= 1,
            @itemType	= @testType,
            @text		= @testText,
            @resultID	= @testResult OUTPUT

    SELECT	* 
    FROM	dbo.psl_languageLookupTexts
    WHERE	id = @testResult;
ROLLBACK TRANSACTION

Данная практика позволит определить не только компиляцию запроса, но и его поведение во время выполнения – корректность выдачи, наличие ошибок и т.д. И вся итерация занимает пару секунд! Сравним с отладкой бизнес-логики на прикладном уровне C# – небольшое изменение в менеджере тянет за собой минутную перекомпиляцию всего сайта. И таких итераций может быть много.

Заключение

Итак, была рассмотрена польза от аккуратного ведения кода, приведены примеры более наглядного форматирования, которое выделяет как блочную структуру, так и группирует отдельные операнды по одной вертикальной линии. Далее были рассмотрены практики разработки SQL, которые позволяют снизить зависимость прикладного кода (Сайта, например), от БД и значительно ускорить отладку кода, по сравнению с отладкой на прикладном уровне.

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