Я хочу рассказать о рефакторинге схем баз данных MS SQL Server.

Рефакторинг — изменение во внутренней структуре программного обеспечения, имеющее целью облегчить понимание его работы и упростить модификацию, не затрагивая наблюдаемого поведения.
— Martin Fowler

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


Как понять что настала пора проводить рефакторинг?


Делая что-то в первый раз, вы просто это делаете. Делая что-то аналогичное во второй раз, вы морщитесь от необходимости повторения, но все-таки повторяете то же самое. Делая что-то похожее в третий раз, вы начинаете рефакторинг.
— Don Roberts

Мартин Фаулер ввел понятие «Код с душком», обозначив так код который нужно подвергнуть рефакторингу.
С душком называется код в котором:
  • встречается дублирование
  • есть большие методы
  • существуют методы с большим количеством параметров
  • встречается оператор switch

По аналогии с этим можно выделить общие недостатки схемы базы данных, которые указывают на необходимость применения рефакторинга. К этим недостаткам можно выделить следующие.
  • Многоцелевые столбцы (или столбцы используемые не по назначению). Допустим у нас есть таблица содержащая информацию по заказам. В таблице есть необязательное для заполнения поле InvoiceId типа int. Представим что процесс продаж в компании построен таким образом, что это поле никогда не заполняется. Начиная с нового года менеджерам стало необходимо проставлять у заказов оценку клиента (от 1 до 10 по результатам обзвона). Такого поля в таблице нет и менеджеры начинают вбивать эти данные в поле InvoiceId (например потому, что IT-шники сказали им что на добавление нового поля уйдет целый месяц). Это приведет к проблемам когда поле InvoiceId станет использоваться по назначению.
  • Многоцелевые таблицы. Примером может послужить таблица Customer в которой хранится информация о физических и юридических лицах. В подобном случае неизбежно появляются столбы с NULL значениями.
  • Избыточные данные. Например, наличие поля Адрес клиента в таблице заказов может привести к случаю когда у нескольких заказов одного и того же клиента будут разные адреса.
  • Таблицы с большим количеством столбцов. Наличие большого количества столбцов может означать что в таблице хранятся атрибуты более чем одной сущности. В таком случае вероятно нужно применить рефакторинг «Разбиение таблицы».
  • Многозначные столбцы. Многозначными называются столбцы, в которых в различных позициях представлено несколько разных фрагментов информации. Например в таблице заказов есть поле OrderNumber содержащее данные вида XXX20150908000125. Где XXX — код товара, 20150908 — дата заказа, 000125 — порядковый номер заказа. На практике часто обнаруживается необходимость разбить поле на части, чтобы можно было проще обрабатывать эти поля в виде отдельных элементов.


Несколько полезных советов по применению рефакторинга


  1. Оцените масштаб бедствий.
    Прежде чем что-то менять убедитесь что Вы не сломаете внешние приложения, использующие Вашу базу данных. Если Вам пришлось поддерживать базу данных, которая досталась «по наследству», вероятнее всего Вы не знаете кто (что) и как ее используют. Составьте список приложений, использующих Вашу базу. Попросите, по возможности, коллег разрабатывающих эти приложения выдать Вам список объектов, которые они используют. После чего согласуйте с ними Ваши изменения, договоритесь о совместном тестировании.
    Особое внимание уделите таблицам базы на которые выданы права. Это потенциальный источник проблем.
    Обсудите с коллегами чтобы они вместо таблиц перешли на использование представлений (процедур).
    Когда все обращения к базе будут реализованы посредством процедур/представлений/функций, Вам будет намного легче проводить рефакторинг.
  2. Не делайте много изменений за один раз.
    Чем меньше будет изменение, тем проще будет найти ошибку в случае сбоя.
  3. Проверяйте изменения тестами.
    После каждого изменения запускайте тесты, чтобы убедиться что ничего не сломалось.
  4. Используйте песочницы.
    Не нужно заниматься рефакторингом на продуктиве, даже если изменение ничтожно мало. Используйте для рефакторинга тестовые площадки. После чего проводите полное регрессионное тестирование. И только после этого выполняйте изменение в продуктивной базе данных.


Практические примеры


Я покажу применение некоторых методов рефакторинга на примере базы данных Northwind (ссылка на скачивание).
В качестве инструмента я буду использовать SQL Server Management Studio (SSMS) с установленным плагином SQL Refactor Studio. Данный плагин добавляет в SSMS функции рефакторинга.

Исходная схема




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


После каждого изменения мы будем запускать тест, чтобы убедиться что все по прежнему работает.
Для примера я создал процедуру dbo.RunTests, которая выбирает данные из всех представлений в базе (разумеется это не обеспечивает нам полное покрытие тестами).
Если в процессе работы процедуры не было ошибок, процедура выдает OK, иначе Failed.
CREATE PROCEDURE dbo.RunTests
AS
DECLARE
	@Script nvarchar(max) = '',
	@Failed bit = 0

DECLARE crs CURSOR FOR
SELECT 'IF OBJECT_ID(''tempdb..#tmp'') IS NOT NULL DROP TABLE #tmp

		SELECT * INTO #tmp FROM [' + object_schema_name(o.object_id) + '].[' + o.name + ']'

FROM sys.objects o
WHERE o.type = 'V'

OPEN crs
FETCH NEXT FROM crs INTO @Script
WHILE @@fetch_status = 0
BEGIN

	BEGIN TRY
		EXEC sp_executesql @Script
	END TRY
	BEGIN CATCH
		SET @Failed = 1

		SELECT 'Failed' AS Status, ERROR_MESSAGE() AS Details, @Script AS [Script]
	END CATCH
		
	FETCH NEXT FROM crs INTO @Script
END
CLOSE crs
DEALLOCATE crs

IF @Failed = 0
	SELECT 'OK' AS [Status]

RETURN 0

/*
EXEC dbo.RunTests
*/
GO


Рефакторинг «Переименование объекта»


Не знаю конечно как Вы, но я при создании таблицы даю ей имя в единственном числе (dbo.Entry а не dbo.Entries).
Итак, давайте попробуем переименовать таблицу dbo.Customers в dbo.Customer. Тут есть один неприятный (и очень рутинный) процесс. Нужно переименовать таблицу так чтобы не сломался код использующий ее. Для этого его нужно найти и внести в него исправление. Воспользовавшись стандартным View Dependencies видим что таблица используется в одном преставлении и есть две таблицы ссылающиеся на dbo.Customers.

В принципе, внести исправление в одно представление после переименования таблицы — плевое дело.
Ну что же, в бой! Переименовываем таблицу и запускаем тест (должна сломаться вьюха dbo.Customer and Suppliers by City).
Однако, вместо ожидаемой одной строки тест мне выдал целых пять.



Тогда я решил проверить зависимости таблицы используя расширенный View Dependencies входящий в состав пакета SQL Refactor Studio. Он уже насчитал пять вьюшек (тест показал что сломалось именно пять представлений) и нашел одну процедуру (не покрыта тестами).

Шесть объектов — это уже посерьезнее. А представьте себе что Вам нужно поправить код в 50+ объектах. Вы все еще хотите переименовать таблицу? :) Ручками будет уже тяжело, поэтому, прибегнем к автоматизации.
Воспользуемся функцией Rename входящий в пакет SQL Refactor Studio. Выбираем таблицу в Object Explorer (далее OE), из контекстного меню выбираем пункт SQL Refactor Studio -> Rename. Вводим новое имя (Customer) и нажимаем кнопку Genarate rename script. Тут также есть возможность посмотреть зависимости и снять галочки напротив объектов в которых не нужно производить переименование.

В результате открылась новая вкладка с сформированным скриптом.
Сгенерированный скрипт
use northwind
go
set transaction isolation level serializable
set xact_abort on
go

if object_id('tempdb..#err') is not null
	drop table #err
go
create table #err(flag bit)
go
begin transaction
go

exec sp_rename 'dbo.Customers', 'Customer', 'OBJECT'

go
if (@@error <> 0) and (@@trancount > 0)
	rollback transaction
go
if (@@trancount = 0)
begin
	insert into #err(flag) select cast(1 as bit)
	begin transaction
end
go
ALTER view dbo.[Orders Qry] AS
SELECT Orders.OrderID, Orders.CustomerID, Orders.EmployeeID, Orders.OrderDate, Orders.RequiredDate,
	Orders.ShippedDate, Orders.ShipVia, Orders.Freight, Orders.ShipName, Orders.ShipAddress, Orders.ShipCity,
	Orders.ShipRegion, Orders.ShipPostalCode, Orders.ShipCountry,
	Customer.CompanyName, Customer.Address, Customer.City, Customer.Region, Customer.PostalCode, Customer.Country
FROM Customer INNER JOIN Orders ON Customer.CustomerID = Orders.CustomerID
go
raiserror('update view <dbo.Orders Qry>...', 0, 1) with nowait

go
if (@@error <> 0) and (@@trancount > 0)
	rollback transaction
go
if (@@trancount = 0)
begin
	insert into #err(flag) select cast(1 as bit)
	begin transaction
end
go
ALTER view dbo.[Quarterly Orders] AS
SELECT DISTINCT Customer.CustomerID, Customer.CompanyName, Customer.City, Customer.Country
FROM Customer RIGHT JOIN Orders ON Customer.CustomerID = Orders.CustomerID
WHERE Orders.OrderDate BETWEEN '19970101' And '19971231'
go
raiserror('update view <dbo.Quarterly Orders>...', 0, 1) with nowait

go
if (@@error <> 0) and (@@trancount > 0)
	rollback transaction
go
if (@@trancount = 0)
begin
	insert into #err(flag) select cast(1 as bit)
	begin transaction
end
go
ALTER view dbo.Invoices AS
SELECT Orders.ShipName, Orders.ShipAddress, Orders.ShipCity, Orders.ShipRegion, Orders.ShipPostalCode,
	Orders.ShipCountry, Orders.CustomerID, Customer.CompanyName AS CustomerName, Customer.Address, Customer.City,
	Customer.Region, Customer.PostalCode, Customer.Country,
	(FirstName + ' ' + LastName) AS Salesperson,
	Orders.OrderID, Orders.OrderDate, Orders.RequiredDate, Orders.ShippedDate, Shippers.CompanyName As ShipperName,
	"Order Details".ProductID, Products.ProductName, "Order Details".UnitPrice, "Order Details".Quantity,
	"Order Details".Discount,
	(CONVERT(money,("Order Details".UnitPrice*Quantity*(1-Discount)/100))*100) AS ExtendedPrice, Orders.Freight
FROM 	Shippers INNER JOIN
		(Products INNER JOIN
			(
				(Employees INNER JOIN
					(Customer INNER JOIN Orders ON Customer.CustomerID = Orders.CustomerID)
				ON Employees.EmployeeID = Orders.EmployeeID)
			INNER JOIN "Order Details" ON Orders.OrderID = "Order Details".OrderID)
		ON Products.ProductID = "Order Details".ProductID)
	ON Shippers.ShipperID = Orders.ShipVia
go
raiserror('update view <dbo.Invoices>...', 0, 1) with nowait

go
if (@@error <> 0) and (@@trancount > 0)
	rollback transaction
go
if (@@trancount = 0)
begin
	insert into #err(flag) select cast(1 as bit)
	begin transaction
end
go
ALTER PROCEDURE dbo.CustOrderHist @CustomerID nchar(5)
AS
SELECT ProductName, Total=SUM(Quantity)
FROM Products P, [Order Details] OD, Orders O, Customer C
WHERE C.CustomerID = @CustomerID
AND C.CustomerID = O.CustomerID AND O.OrderID = OD.OrderID AND OD.ProductID = P.ProductID
GROUP BY ProductName
go
raiserror('update storedprocedure <dbo.CustOrderHist>...', 0, 1) with nowait

go
if (@@error <> 0) and (@@trancount > 0)
	rollback transaction
go
if (@@trancount = 0)
begin
	insert into #err(flag) select cast(1 as bit)
	begin transaction
end
go
ALTER view dbo.[Customer and Suppliers by City] AS
SELECT City, CompanyName, ContactName, 'Customers' AS Relationship
FROM Customer
UNION SELECT City, CompanyName, ContactName, 'Suppliers'
FROM Suppliers
--ORDER BY City, CompanyName
go
raiserror('update view <dbo.Customer and Suppliers by City>...', 0, 1) with nowait

go
if (@@error <> 0) and (@@trancount > 0)
	rollback transaction
go
if (@@trancount = 0)
begin
	insert into #err(flag) select cast(1 as bit)
	begin transaction
end
go
ALTER view dbo.[Sales Totals by Amount] AS
SELECT "Order Subtotals".Subtotal AS SaleAmount, Orders.OrderID, Customer.CompanyName, Orders.ShippedDate
FROM 	Customer INNER JOIN
		(Orders INNER JOIN "Order Subtotals" ON Orders.OrderID = "Order Subtotals".OrderID)
	ON Customer.CustomerID = Orders.CustomerID
WHERE ("Order Subtotals".Subtotal >2500) AND (Orders.ShippedDate BETWEEN '19970101' And '19971231')
go
raiserror('update view <dbo.Sales Totals by Amount>...', 0, 1) with nowait

go
if (@@error <> 0) and (@@trancount > 0)
	rollback transaction
go
if (@@trancount = 0)
begin
	insert into #err(flag) select cast(1 as bit)
	begin transaction
end
go
if exists (select * from #err)
begin
	print 'the database <northwind> update failed'
	rollback transaction
end
else
begin
	print 'the database <northwind> update succeeded'
	commit transaction
end

go
     


Запускаем скрипт на выполнение.
Запускаем тест.
Вуаля! Мы переименовали таблицу и не сломали существующий код.
Поступим также со всеми таблицами, имена которых заканчиваются на s. Это ведь так просто, неправда ли?

Рефакторинг «Добавление поисковой таблицы»


Эта операция позволяет создать поисковую таблицу для существующего столбца.
Необходимость этой операции может быть обусловлена следующими причинами:
  • Введение ссылочной целостности для обеспечения качества данных;
  • Предоставление подробных описаний. Например, может понадобится добавить в описание той или иной сущности новый атрибут. Если сущность при этой не выделена в таблицу — придется добавлять этот атрибут в нужные таблицы, что приведет к денормализации схемы.


Посмотрим еще раз на нашу таблицу dbo.Customer. Вас не смущает наличие полей City, Region и Country в одном месте? Это ведь атрибуты одной сущности.


В таблице dbo.Employees та же беда. На вид явное нарушение 3-й нормальной формы.

Давайте начинать исправлять дело следующим образом:
1. Создадим справочник dbo.City(CityId, CityName)
2. В таблице dbo.Customer добавим поле CityId.
3. Создадим внешний ключ.

Опять же, для экономии времени, используем функцию Add Lookup Table. В OE выбираем поле City таблицы dbo.Customer и в контекстном меню вызываем пункт SQL Refactor Studio -> Add Lookup Table.

В появившемся окне заполняем поля. Жмем Next, потом Finish и в новом окне формируется скрипт.
Скрипт создает таблицу dbo.City, заполняет ее данными, создает поле CityId в таблице dbo.Customer, создает внешний ключ.
Сгенерированный скрипт
use northwind
go
-- Step 1. Create lookup table.
CREATE TABLE dbo.City (
CityId INT NOT NULL identity(1, 1)
,CityName NVARCHAR(15) NULL
,CONSTRAINT PK_City PRIMARY KEY CLUSTERED (CityId)
,CONSTRAINT City_ixCityName UNIQUE (CityName)
)
GO

-- Step 2. Fill lookup table.
INSERT dbo.City (CityName)
SELECT DISTINCT City
FROM dbo.Customer
GO

-- Step 3. Add column.
ALTER TABLE dbo.Customer ADD CityId INT NULL
GO

-- Step 4. Update table dbo.Customer.
UPDATE s
SET s.CityId = t.CityId
FROM dbo.Customer s
INNER JOIN dbo.City t ON s.City = t.CityName
GO

-- Step 5. Create foreign key constraint.
ALTER TABLE dbo.Customer ADD CONSTRAINT FK_Customer_City FOREIGN KEY (CityId) REFERENCES dbo.City (CityId)
GO
     


Запускаем скрипт и тесты (хотя тут мы вроде ничего не должны были сломать).
Все готово. Идем далее.


Рефакторинг «Перемещение полей»


Итак, с полем City мы разобрались. Осталось разобраться с полями Region и Country. Данные поля являются атрибутами сущности City. Так давайте же перенесем их из таблицы dbo.Customer в dbo.City.
Опять же, SQL Refactor Studio предоставляет функцию Move Columns. Ею и воспользуемся!
Выбираем в OE таблицу dbo.Customer, в контекстном меню выбираем пункт «SQL Refactor Studio -> Move columns». В появившемся диалоге, в выпадающем списке, выбираем таблицу dbo.City. Переносим поля Region и Country в dbo.City.
Если при переносе поля, вы получите сообщение о том, что нельзя переместить поле на котором построен индекс — удалите на время этот индекс.


Нажимаем Next, потом Finish. Получаем скрипт в новом окне.
Скрипт создает поля Region и Country в таблице dbo.City и заполняет их данными.
В скрипте есть также закомментированный код удаляющий поля и приведен список объектов в которых нужно внести изменения.
Давайте не будем сейчас удалять поля, сделаем это на следующем шаге.
Сгенерированный скрипт
USE northwind
GO

-- STEP 1. Add new column(s) --
IF NOT EXISTS (
SELECT *
FROM syscolumns s
WHERE s.NAME = 'Region'
AND s.id = object_id(N'dbo.City')
)
BEGIN
ALTER TABLE dbo.City ADD Region NVARCHAR(15) NULL
END
GO

IF NOT EXISTS (
SELECT *
FROM syscolumns s
WHERE s.NAME = 'Country'
AND s.id = object_id(N'dbo.City')
)
BEGIN
ALTER TABLE dbo.City ADD Country NVARCHAR(15) NULL
END
GO


GO

-- STEP 2. Copy data --
-- (You can modify this query if needed)
SET IDENTITY_INSERT dbo.City ON

INSERT INTO dbo.City WITH (TABLOCKX) (CityId)
SELECT CityId
FROM dbo.Customer src
WHERE NOT EXISTS (
SELECT *
FROM dbo.City dest
WHERE src.CityId = dest.CityId
)

SET IDENTITY_INSERT dbo.City OFF

UPDATE dest
WITH (TABLOCKX)

SET dest.Region = src.Region
,dest.Country = src.Country
FROM dbo.City dest
INNER JOIN dbo.Customer src ON (src.CityId = dest.CityId)
GO

-- STEP 3. Check and modify this dependent objects --
/*
northwind.dbo.[Orders Qry] /*View*/
northwind.dbo.[Quarterly Orders] /*View*/
northwind.dbo.Invoices /*View*/
northwind.dbo.CustOrderHist /*StoredProcedure*/
northwind.dbo.[Customer and Suppliers by City] /*View*/
northwind.dbo.[Sales Totals by Amount] /*View*/

*/
-- STEP 4. Drop column(s) --
-- (Uncomment or run separately this query)
/*
alter table dbo.Customer drop column Region
alter table dbo.Customer drop column Country
*/
GO
     


Выполняем скрипт и тесты.
Переходим к следующему шагу.


Рефакторинг «Удаление объекта»


Выполняя предыдущие рефакторинги, мы оставили немного мусора (поля City, Region, Country в таблице dbo.Customer).
Давайте наводить чистоту! Но если мы просто так удалим поля, у нас опять все сломается.
Можно воспользоваться рефакторингом Encapsulate Table With View.
Создадим представление dbo.CustomerV и заменим использование таблицы его представлением во всей базе данных.
CREATE VIEW dbo.CustomerV
AS
SELECT 
 c.CustomerID,
 c.CompanyName,
 c.ContactName,
 c.ContactTitle,
 c.Address,
 ct.CityName City,
 ct.Region,
 c.PostalCode,
 ct.Country,
 c.Phone,
 c.Fax,
 c.CityId
FROM dbo.Customer AS c
LEFT JOIN dbo.City AS ct
	ON c.CityId = ct.CityId 


Далее, при помощи View Dependencies смотрим зависимости для таблицы dbo.Customer:


Просматриваем каждый объект. Если в каком-либо объекте используются наши поля, скриптуем объект (кнопка Script object на тулбаре) и вносим изменения.
В результате у меня получился вот такой скрипт:

Encapsulate Table With View
ALTER view dbo.[Sales Totals by Amount] AS
SELECT st.Subtotal AS SaleAmount, o.OrderID, c.CompanyName, o.ShippedDate
FROM dbo.CustomerV c 
JOIN (
	dbo.Orders o
	JOIN "Order Subtotals" st 
		ON o.OrderID = st.OrderID
)
	ON c.CustomerID = o.CustomerID
WHERE (st.Subtotal >2500) AND (o.ShippedDate BETWEEN '19970101' And '19971231')

GO

ALTER view dbo.[Quarterly Orders] 
AS
SELECT DISTINCT c.CustomerID, c.CompanyName, c.City, c.Country
FROM dbo.CustomerV c 
RIGHT JOIN dbo.Orders o 
	ON c.CustomerID = o.CustomerID
WHERE
	o.OrderDate BETWEEN '19970101' And '19971231'

GO

ALTER view dbo.[Orders Qry] 
AS
SELECT o.OrderID, o.CustomerID, o.EmployeeID, o.OrderDate, o.RequiredDate,
	o.ShippedDate, o.ShipVia, o.Freight, o.ShipName, o.ShipAddress, o.ShipCity,
	o.ShipRegion, o.ShipPostalCode, o.ShipCountry,
	c.CompanyName, c.Address, c.City, c.Region, c.PostalCode, c.Country
FROM dbo.CustomerV c 
INNER JOIN dbo.Orders o 
	ON c.CustomerID = o.CustomerID

GO

ALTER view dbo.Invoices AS
SELECT Orders.ShipName, Orders.ShipAddress, Orders.ShipCity, Orders.ShipRegion, Orders.ShipPostalCode,
	Orders.ShipCountry, Orders.CustomerID, Customer.CompanyName AS CustomerName, Customer.Address, Customer.City,
	Customer.Region, Customer.PostalCode, Customer.Country,
	(FirstName + ' ' + LastName) AS Salesperson,
	Orders.OrderID, Orders.OrderDate, Orders.RequiredDate, Orders.ShippedDate, Shippers.CompanyName As ShipperName,
	"Order Details".ProductID, Products.ProductName, "Order Details".UnitPrice, "Order Details".Quantity,
	"Order Details".Discount,
	(CONVERT(money,("Order Details".UnitPrice * Quantity*(1-Discount)/100))*100) AS ExtendedPrice, Orders.Freight
FROM 	Shippers INNER JOIN
		(Products INNER JOIN
			(
				(Employees INNER JOIN
					(dbo.CustomerV Customer INNER JOIN Orders ON Customer.CustomerID = Orders.CustomerID)
				ON Employees.EmployeeID = Orders.EmployeeID)
			INNER JOIN "Order Details" ON Orders.OrderID = "Order Details".OrderID)
		ON Products.ProductID = "Order Details".ProductID)
	ON Shippers.ShipperID = Orders.ShipVia

GO

ALTER view dbo.[Customer and Suppliers by City] AS
SELECT City, CompanyName, ContactName, 'Customers' AS Relationship
FROM dbo.CustomerV
UNION SELECT City, CompanyName, ContactName, 'Suppliers'
FROM Suppliers
--ORDER BY City, CompanyName

GO
     


Запускаем скрипт, после чего удаляем поля и прогоняем тесты.
Если все ОК — Вы молодец, все аккуратно сделали.


Рефакторинг «Добавление методов CRUD»


Данный рефакторинг предусматривает создание хранимых процедур, обеспечивающих доступ (SELECT, INSERT, UPDATE, DELETE) к таблицам базы данных.
Можно выделить следующие причины использовать этот рефакторинг:
  • Скрыть от внешних приложений структуру базы данных. Внешние приложения будут обращаться к базе данных только посредством хранимых процедур. Это позволит с легкостью изменять структуру базы данных без изменения внешних приложений.
  • Реализация в процедурах дополнительных проверок (бизнес-логики, прав доступа и т.д.);
  • Сохранение информации о том кто и когда какие данные запрашивал;
  • Проблемы с производительностью эффективнее решать когда SQL-код находится в базе данных.


Итак, давайте создадим для нашей замученной таблицы dbo.Customer методы доступа. Воспользуемся методом Add CRUD Methods из пакета SQL Refactor Studio. Выбираем в OE таблицу, далее в контекстном меню выбираем пункт SQL Refactor Studio -> Add CRUD Methods.

В появившемся диалоге выбираем какие методы нам нужно создать. При необходимости меняем названия методов. Жмем Next. При желании можно настроить права на процедуры, для этого нужно отметить нужные роли. Нажимаем Finish и получаем скрипт с хранимыми процедурами. Выполняем скрипт.

Сгенерированный скрипт
IF (object_id(N'dbo.Customer_Create') IS NULL)
BEGIN
	EXEC ('create procedure dbo.Customer_Create as return 0')
END
GO
-- =============================================
--
-- 	dbo.Customer_Create
--
--	Create method.
--
-- 	Date: 07.09.2015, @HabraUser
--
-- =============================================
ALTER PROCEDURE dbo.Customer_Create 
 @CustomerID NCHAR(5)
,@CompanyName NVARCHAR(40)
,@ContactName NVARCHAR(30) = NULL
,@ContactTitle NVARCHAR(30) = NULL
,@Address NVARCHAR(60) = NULL
,@PostalCode NVARCHAR(10) = NULL
,@Phone NVARCHAR(24) = NULL
,@Fax NVARCHAR(24) = NULL
,@CityId INT = NULL
AS
BEGIN
SET NOCOUNT ON

INSERT INTO dbo.Customer (
	CustomerID
	,CompanyName
	,ContactName
	,ContactTitle
	,Address
	,PostalCode
	,Phone
	,Fax
	,CityId
)
VALUES (
	@CustomerID
	,@CompanyName
	,@ContactName
	,@ContactTitle
	,@Address
	,@PostalCode
	,@Phone
	,@Fax
	,@CityId
)

	RETURN 0
END
/*
declare
	@CustomerID NChar(5),	
	@CompanyName NVarChar(40),	
	@ContactName NVarChar(30),	
	@ContactTitle NVarChar(30),	
	@Address NVarChar(60),	
	@PostalCode NVarChar(10),	
	@Phone NVarChar(24),	
	@Fax NVarChar(24),	
	@CityId Int	

select
	@CustomerID = 'CustomerID',	
	@CompanyName = 'CompanyName',	
	@ContactName = 'ContactName',	
	@ContactTitle = 'ContactTitle',	
	@Address = 'Address',	
	@PostalCode = 'PostalCode',	
	@Phone = 'Phone',	
	@Fax = 'Fax',	
	@CityId = null	

exec  dbo.Customer_Create
	@CustomerID = @CustomerID,	
	@CompanyName = @CompanyName,	
	@ContactName = @ContactName,	
	@ContactTitle = @ContactTitle,	
	@Address = @Address,	
	@PostalCode = @PostalCode,	
	@Phone = @Phone,	
	@Fax = @Fax,	
	@CityId = @CityId	

*/
GO

IF (object_id(N'dbo.Customer_Get') IS NULL)
BEGIN
EXEC ('create procedure dbo.Customer_Get as return 0')
END
GO

-- =============================================
--
-- 	dbo.Customer_Get
--
--	Read method.
--
-- 	Date: 07.09.2015, @HabraUser
--
-- =============================================
ALTER PROCEDURE dbo.Customer_Get
	@CustomerID NCHAR(5)
AS
BEGIN
SET NOCOUNT ON

SELECT 
 CustomerID
,CompanyName
,ContactName
,ContactTitle
,Address
,PostalCode
,Phone
,Fax
,CityId
FROM dbo.Customer
WHERE
	CustomerID = @CustomerID

RETURN 0
END
/*
declare
	@CustomerID NChar(5)	

select
	@CustomerID = ?	

exec dbo.Customer_Get
	@CustomerID = @CustomerID	
*/
GO

IF (object_id(N'dbo.Customer_Save') IS NULL)
BEGIN
EXEC ('create procedure dbo.Customer_Save as return 0')
END
GO

-- =============================================
--
-- 	dbo.Customer_Save
--
--	Update method.
--
-- 	Date: 07.09.2015, @HabraUser
--
-- =============================================
ALTER PROCEDURE dbo.Customer_Save 
 @CustomerID NCHAR(5)
,@CompanyName NVARCHAR(40)
,@ContactName NVARCHAR(30) = NULL
,@ContactTitle NVARCHAR(30) = NULL
,@Address NVARCHAR(60) = NULL
,@PostalCode NVARCHAR(10) = NULL
,@Phone NVARCHAR(24) = NULL
,@Fax NVARCHAR(24) = NULL
,@CityId INT = NULL
AS
BEGIN
SET NOCOUNT ON

UPDATE t
	SET t.CompanyName = @CompanyName
	,t.ContactName = @ContactName
	,t.ContactTitle = @ContactTitle
	,t.Address = @Address
	,t.PostalCode = @PostalCode
	,t.Phone = @Phone
	,t.Fax = @Fax
	,t.CityId = @CityId
FROM dbo.Customer AS t
WHERE
	t.CustomerID = @CustomerID

RETURN 0
END
/*
	set nocount on
	set quoted_identifier, ansi_nulls, ansi_warnings, arithabort, concat_null_yields_null, ansi_padding on
	set numeric_roundabort off
	set transaction isolation level read uncommitted

	declare
		@CustomerID NChar(5),	
		@CompanyName NVarChar(40),	
		@ContactName NVarChar(30),	
		@ContactTitle NVarChar(30),	
		@Address NVarChar(60),	
		@PostalCode NVarChar(10),	
		@Phone NVarChar(24),	
		@Fax NVarChar(24),	
		@CityId Int	

	select
		@CustomerID = 'CustomerID',	
		@CompanyName = 'CompanyName',	
		@ContactName = 'ContactName',	
		@ContactTitle = 'ContactTitle',	
		@Address = 'Address',	
		@PostalCode = 'PostalCode',	
		@Phone = 'Phone',	
		@Fax = 'Fax',	
		@CityId = null	


	begin try
		begin tran
			exec dbo.Customer_Save
				@CustomerID = @CustomerID,	
				@CompanyName = @CompanyName,	
				@ContactName = @ContactName,	
				@ContactTitle = @ContactTitle,	
				@Address = @Address,	
				@PostalCode = @PostalCode,	
				@Phone = @Phone,	
				@Fax = @Fax,	
				@CityId = @CityId	
	
			select t.*
			from dbo.Customer as t
			where	
				t.CustomerID = @CustomerID

			if @@trancount > 0
				rollback tran

	end try
	begin catch
		if @@trancount > 0
			rollback tran
	
		declare
			@err nvarchar(2000)
		
		  set @err = 
			  'login: ' + suser_sname() + char(10)
			+ 'ErrorNumber: ' + cast(isnull(error_number(), 0) as varchar) + char(10)
			+ 'ErrorProcedure: ' + isnull(error_procedure(), '') + char(10)
			+ 'ErrorLine: ' + cast(isnull(error_line(), 0) as varchar) + char(10)
			+ 'ErrorMessage: ' + isnull(error_message(), '') + char(10)
			+ 'Date: ' + cast(getdate() as varchar) + char(10)
	
		print @err
		raiserror(@err, 16, 1)
	end catch
*/
GO

IF (object_id(N'dbo.Customer_Del') IS NULL)
BEGIN
EXEC ('create procedure dbo.Customer_Del as return 0')
END
GO

-- =============================================
--
-- 	dbo.Customer_Del
--
--	Delete method.
--
-- 	Date: 07.09.2015, @HabraUser
--
-- =============================================
ALTER PROCEDURE dbo.Customer_Del @CustomerID NCHAR(5)
AS
BEGIN
SET NOCOUNT ON

BEGIN TRY
	BEGIN TRAN

	/* uncomment if needed
		delete from dbo.CustomerCustomerDemo where CustomerID = ?
		delete from dbo.Orders where CustomerID = ?
 
	*/
	DELETE
	FROM dbo.Customer
	WHERE CustomerID = @CustomerID

	COMMIT TRAN
END TRY

BEGIN CATCH
IF @@trancount > 0
	ROLLBACK TRAN

-- catch exception (add you code here)
DECLARE @err NVARCHAR(2000)

SET @err = ERROR_MESSAGE()

RAISERROR (@err, 16, 1)
END CATCH

RETURN 0
END
/*
declare
	@CustomerID NChar(5)	

select
	@CustomerID = ?	

exec dbo.Customer_Del
	@CustomerID = @CustomerID	

*/
GO

IF (object_id(N'dbo.Customer_List') IS NULL)
BEGIN
EXEC ('create procedure dbo.Customer_List as return 0')
END
GO

-- =============================================
--
-- 	dbo.Customer_List
--
--	List method.
--
-- 	Date: 07.09.2015, @HabraUser
--
-- =============================================
ALTER PROCEDURE dbo.Customer_List
AS
BEGIN
SET NOCOUNT ON

SELECT 
	 CustomerID
	,CompanyName
	,ContactName
	,ContactTitle
	,Address
	,PostalCode
	,Phone
	,Fax
	,CityId
FROM dbo.Customer
/* uncomment if needed
	left join dbo.City as t1 on t1.CityId = t.CityId
*/
RETURN 0
END
/*
exec dbo.Customer_List
*/
GO
     



Если Вам нужно поправить тело генерируемых процедур, заходим в настройки.



Каждая процедура представляет собой шаблон T4. Про T4 можно почитать тут и тут.


Рефакторинг «Введение триггера для накопления исторических данных»


Эта операция позволяет ввести новый триггер, предназначенный для накопления информации об изменениях в данных в целях изучения истории внесения изменений или проведения аудита.
Необходимость в применении операции «Введение триггера для накопления исторических данных» в основном обусловлена требованием передать функции отслеживания изменений в данных самой базе данных. Такой подход гарантирует, что в случае модификации важных данных в любом внешнем приложении это изменение можно будет отследить и подвергнуть аудиту.
Единственным недостатком, на мой взгляд, является тот факт что наличие триггера будет увеличивать время выполнения операции DML.
Как альтернативу данному методу можно рассмотреть Change Data Capture (работает асинхронно, тем самым не увеличивает время операции, но имеет ряд особенностей).

Давайте для таблицы dbo.Customer применим данный рефакторинг.
Выбираем таблицу в OE, выбираем в контекстном меню пункт SQL Refactor Studio — Introduce Trigger for History. Выбираем поля таблицы для отслеживания изменений.

Жмем Next. Изменяем при необходимости имя создаваемой таблицы и триггера.


Жмем Finish и получаем скрипт. Скрипт создает триггер и таблицу для хранения истории изменений.

Сгенерированный скрипт
CREATE TABLE [dbo].[CustomerHistory] (
[id] [bigint] IDENTITY NOT NULL
,[action_type] [char](1) NOT NULL
,[modified_date] [datetime] CONSTRAINT [DF_CustomerHistory_modified_date] DEFAULT getdate()
,[modified_login] [sysname] CONSTRAINT [DF_CustomerHistory_modified_login] DEFAULT suser_sname()
,[host_name] [nvarchar](128) CONSTRAINT [DF_CustomerHistory_host_name] DEFAULT host_name()
,[program_name] [nvarchar](128) CONSTRAINT [DF_CustomerHistory_program_name] DEFAULT program_name()
,[CompanyName_old] [nvarchar](40)
,[CompanyName_new] [nvarchar](40)
,[ContactName_old] [nvarchar](30)
,[ContactName_new] [nvarchar](30)
,[ContactTitle_old] [nvarchar](30)
,[ContactTitle_new] [nvarchar](30)
,[Address_old] [nvarchar](60)
,[Address_new] [nvarchar](60)
,[PostalCode_old] [nvarchar](10)
,[PostalCode_new] [nvarchar](10)
,[Phone_old] [nvarchar](24)
,[Phone_new] [nvarchar](24)
,[Fax_old] [nvarchar](24)
,[Fax_new] [nvarchar](24)
,[CityId_old] [int]
,[CityId_new] [int]
,[CustomerID_old] [nchar](5)
,[CustomerID_new] [nchar](5)
,CONSTRAINT [PK_CustomerHistory] PRIMARY KEY ([id])
)
GO

CREATE TRIGGER [dbo].[trg_CustomerHistory] ON [dbo].[Customer]
AFTER INSERT, DELETE, UPDATE
AS
SET NOCOUNT ON

DECLARE @action_type CHAR(1)

IF EXISTS (SELECT *FROM inserted) AND EXISTS (SELECT * FROM deleted)
	SET @action_type = 'U'
ELSE IF EXISTS (SELECT * FROM inserted) AND NOT EXISTS (SELECT * FROM deleted)
	SET @action_type = 'I'
ELSE IF NOT EXISTS (SELECT * FROM inserted) AND EXISTS (SELECT * FROM deleted)
	SET @action_type = 'D'

INSERT INTO dbo.CustomerHistory (
	 action_type
	,CompanyName_old
	,CompanyName_new
	,ContactName_old
	,ContactName_new
	,ContactTitle_old
	,ContactTitle_new
	,Address_old
	,Address_new
	,PostalCode_old
	,PostalCode_new
	,Phone_old
	,Phone_new
	,Fax_old
	,Fax_new
	,CityId_old
	,CityId_new
	,CustomerID_old
	,CustomerID_new
)
SELECT 
	 @action_type
	,d.CompanyName
	,i.CompanyName
	,d.ContactName
	,i.ContactName
	,d.ContactTitle
	,i.ContactTitle
	,d.Address
	,i.Address
	,d.PostalCode
	,i.PostalCode
	,d.Phone
	,i.Phone
	,d.Fax
	,i.Fax
	,d.CityId
	,i.CityId
	,d.CustomerID
	,i.CustomerID
FROM inserted i
FULL OUTER JOIN deleted d 
	ON (i.CustomerID = d.CustomerID)
GO
     



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


Полезные ресурсы


Комментарии (53)


  1. lair
    10.09.2015 00:46
    +3

    А почему вы никак, вообще ни словом не упоминаете об актуализации и тестировании всех систем, взаимодействующих с вашей БД?


    1. vorobyev_evgeney
      11.09.2015 11:55

      Спасибо! Действительно упустил. Добавил небольшой пункт в советы.


      1. lair
        11.09.2015 12:01

        «Небольшой пункт», серьезно? Половина Database Refactoring посвящена тому, как решать эту проблему, а вы обошлись «небольшим пунктом»?

        Составьте список приложений, использующих Вашу базу.

        На основании чего?

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

        А если это не «коллеги», а другая компания, давно ушедшая с контракта? А если такого списка нет?

        Обсудите с коллегами чтобы они вместо таблиц перешли на использование представлений (процедур).
        Когда все обращения к базе будут реализованы посредством процедур/представлений/функций, Вам будет намного легче проводить рефакторинг.

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

        Ну и да, добротный гвоздь в крышку: а если код приложения недоступен (как следствие, его нельзя ни модифицировать, ни проанализировать)?


        1. vorobyev_evgeney
          11.09.2015 12:12

          Проблем действительно много. И не всем уделено внимание в книге Database Refactoring. У каждого встречаются свои тараканы.
          В статье я лишь показал использование базовых методов рефакторинга.
          Давайте соберемся и напишем отдельную статью на эту тему?


          1. lair
            11.09.2015 15:32
            +1

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

            Будем честными, для меня ваша статья выглядит как «методы рефакторинга, доступные в SQL Refactor Studio». Например, добавление исторических таблиц/триггеров — это вообще не рефакторинг.

            Но самое важное то, что для рефакторинга критична проверка сохранения работоспособности, а вы эту тему, как уже сказано, не затронули практически.


        1. Delphinum
          11.09.2015 12:22

          На основании чего?

          Посвятите, пожалуйста, в ужасающий пример невозможности определения приложений, использующих базу.


          1. lair
            11.09.2015 12:36

            Почему же сразу «невозможности»?

            Вот вы пришли на проект, он пять лет развернут в продуктиве в достаточно крупной (и весьма безалаберной) организации. Как вы узнаете, какие приложения используют его БД?


            1. Delphinum
              11.09.2015 12:44

              Отключу БД.


              1. lair
                11.09.2015 12:46

                В продуктиве? Очень мило. На сколько?


                1. Delphinum
                  11.09.2015 12:49

                  Как правило нескольких минут достаточно, чтоб начались агрессивные звонки в тех.отдел с просьбой вернуть все как было. Далее можно начинать рефакторить, а «забытые приложения» уже сами всплывут. Поверьте, этот подход куда действенее, чем попытка отыскать пользователей по «горячим следам».


                  1. lair
                    11.09.2015 15:27

                    За несколько минут по звонкам вы определите основные line-of-business (которые и так известны, чего уж), и менеджмент которых придет к вам со строгим наказом никогда так больше не делать.

                    А что делать со всеми остальными малозаметными интеграциями, которые упадут через два месяца после выкатки вашего рефакторинга в продуктив?


                    1. Delphinum
                      11.09.2015 17:51

                      А что делать со всеми остальными малозаметными интеграциями, которые упадут через два месяца после выкатки вашего рефакторинга в продуктив?

                      А что тут сделать. Варианта два:
                      1. Пытаться определить их «по хорошему», что приведет к трате большого количества времени и вопросом начальства вида — чем это ты столько времени занимаешься?
                      2. Починить проблемы по мере поступления.

                      Я двумя руками за первый вариант, но чаще всего он ни к чему не приведет, тем более потребность в рефакторинге начальство никогда не испытывает, а если что то начинает ломаться, всегда дадут время это исправить ;)


                      1. lair
                        11.09.2015 18:09

                        если что то начинает ломаться, всегда дадут время это исправить

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


                        1. Delphinum
                          11.09.2015 18:24

                          Потому так редко делается рефакторинг в информационных системах, связанных с гос. структурами. Хотеть я этого не хочу, но рефакторинг делал (когда работал с гос. структурами), ибо 1 ночь понервничать лучше, чем год копаться в говнокоде.


                          1. lair
                            11.09.2015 18:59

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

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


                            1. Delphinum
                              11.09.2015 20:37

                              А вы думаете, контролирующие ведомства бывают только в госухе?

                              А не думаю, я уверен что в госухе нет контролирующих ведомств. Но сейчас не об этом.
                              Просто это тяжелее и медленнее.

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

                              Не могу уловить вашу позицию. Вы с моими словами не согласны?


                              1. lair
                                11.09.2015 20:44

                                не думаю, я уверен что в госухе нет контролирующих ведомств.

                                Зря. Но речь правда не об этом.

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

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

                                Не могу уловить вашу позицию. Вы с моими словами не согласны?

                                Конечно, не согласен. Нельзя делать опасные для работоспособности продуктивной системы действия без согласия заказчика.


                                1. Delphinum
                                  11.09.2015 20:50

                                  Понимаете ли в чем дело, если начальству рефакторинг не нужен, то единственное обоснование, которое вы сможете выдать для остановки системы в продуктиве — ваша же ошибка. За которую вам же и влетит.

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

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


                                  1. lair
                                    11.09.2015 20:55

                                    Таки вы хотите и на пеньке посидеть, и ватрушку вкусить. Мой опыт подсказывает, что не получится.

                                    А мой опыт показывает, что если рефакторинг заказчику выгоден, то заказчик на него можно уговорить. А если невыгоден, то зачем его проводить?

                                    Делаете его вы сугубо для того, чтобы вам не копаться в говнокоде.

                                    Вот именно поэтому вы не можете взять и остановить ради своего рефакторинга продуктивную систему.


                                    1. lair
                                      11.09.2015 20:58

                                      Точнее, конечно, не заказчику (выгоден рефакторинг), а вашему начальству. А уже ваше начальство договаривается с заказчиком, используя соответствующие формулировки.


                                      1. Delphinum
                                        11.09.2015 21:01

                                        Мое начальство не бум бум в IT, ему самому рефакторинг до лампочки.


                                        1. lair
                                          11.09.2015 21:04

                                          Значит либо заводите такое начальство, которое бум-бум, либо учитесь объяснять тем, кто не бум-бум. А иначе так и будете делать все за свой счет.


                                          1. Delphinum
                                            11.09.2015 21:07

                                            Значит либо заводите такое начальство, которое бум-бум, либо учитесь объяснять тем, кто не бум-бум.

                                            Всех уволить, а на их места хороших набрать? )


                                    1. Delphinum
                                      11.09.2015 21:00

                                      А мой опыт показывает, что если рефакторинг заказчику выгоден, то заказчик на него можно уговорить. А если невыгоден, то зачем его проводить?

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

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


                                      1. lair
                                        11.09.2015 21:03

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

                                        А просто не надо делать рефакторинг, когда система «уже работает». Включите его в косты доработки (а лучше — в косты разработки) — и все. И не будет никакого «в будущем».

                                        Ну как же не могу, могу, и даже приходилось делать.

                                        Ну то есть я правильно понял, вы можете, не объясняя причин (и не неся ответственности) остановить продуктивную систему (что обычно влечет за собой те или иные потери заказчика)?


                                        1. Delphinum
                                          11.09.2015 21:08

                                          А просто не надо делать рефакторинг, когда система «уже работает». Включите его в косты доработки (а лучше — в косты разработки) — и все. И не будет никакого «в будущем».

                                          Как мне включить его в уже оплаченную, разработанную (не мной) и запущенную систему?
                                          Ну то есть я правильно понял, вы можете, не объясняя причин (и не неся ответственности) остановить продуктивную систему (что обычно влечет за собой те или иные потери заказчика)?

                                          В идеале да.


                                          1. lair
                                            11.09.2015 21:23

                                            Как мне включить его в уже оплаченную, разработанную (не мной) и запущенную систему?

                                            Никак. Если она оплачена, разработана и запущена, просто не надо ее трогать.

                                            В идеале да.

                                            И вам не кажется, что это безответственно по отношению к заказчику?

                                            Всех уволить, а на их места хороших набрать?

                                            Да нет, достаточно самому уволиться.


                                            1. Delphinum
                                              11.09.2015 21:34

                                              просто не надо ее трогать.

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

                                              Безответственно по отношению к заказчику, бережно собирать и культивировать костыли в их говнокоде. Так как заказчик человек не сведующий, он должен быть мне благодарен за то, что я чищу его систему (обычно бесплатно), ценою десятка минут неработоспособности.
                                              Да нет, достаточно самому уволиться.

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


                                              1. lair
                                                11.09.2015 21:42

                                                Поддерживать то надо, или говнокод это «не так уж и плохо»?

                                                Если надо поддерживать — включаете в косты поддержки.

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

                                                Нет, не должен быть. Вы не думали, что этот десяток минут неработоспособности может выйти ему дороже, чем весь выигрыш от вашей «чистки»?

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

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


                                                1. Delphinum
                                                  11.09.2015 21:45

                                                  Если надо поддерживать — включаете в косты поддержки.

                                                  На что заказчик спросит — а чего так дорого?
                                                  Нет, не должен быть. Вы не думали, что этот десяток минут неработоспособности может выйти ему дороже, чем весь выигрыш от вашей «чистки»?

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

                                                  На этих словах, думается, можно закончить наш милый диалог )


                                                  1. lair
                                                    11.09.2015 21:47

                                                    На что заказчик спросит — а чего так дорого?

                                                    Потому что ваша работа столько стоит. Если ваш заказчик не понимает в IT, у вас нет никакого другого способа ему что-то объяснить.

                                                    Я не рефакторю системы таким способом, если это может выйти заказчику боком.

                                                    А откуда вы знаете, может, или нет?


                                                    1. Delphinum
                                                      11.09.2015 21:54

                                                      Потому что ваша работа столько стоит

                                                      Вам нужно сказки писать, чесслово ) Не хочу вас обидеть, но мне кажется, что вы немного лукавите. С одной стороны вы боитесь остановить работу ПО заказчика чтобы отчистить его от говнокода, с другой стороны вы предлагаете ставить заказчику условия и продавать ему то, что ему не нужно )
                                                      А откуда вы знаете, может, или нет?

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

                                                      Может все таки закончим, а? Думается у нас разная позиция и друг друга мы убедить не сможем.


                                                      1. lair
                                                        11.09.2015 21:58

                                                        с другой стороны вы предлагаете ставить заказчику условия и продавать ему то, что ему не нужно )

                                                        Я не предлагаю продавать то, что не нужно. И да, я предлагаю ставить ему условия, потому что работать без условий — это рабство.

                                                        Если о системе нет вообще никакой инфы, то вы ее не отрефакторите никакими методами.

                                                        Ну можно же сначала собрать информацию. А если можно ее собрать, то зачем останавливать систему?

                                                        Напомню, вы предлагаете остановить БД, чтобы узнать, кто к ней подключен. Если вы не знаете, кто к ней подключен (а иначе зачем ее останавливать) — это означает, что вы не знаете, какие последствия будут у останова.


                                                        1. Delphinum
                                                          11.09.2015 22:15

                                                          Я не предлагаю продавать то, что не нужно

                                                          Повторюсь, заказчику рефакторинг не нужен.
                                                          И да, я предлагаю ставить ему условия, потому что работать без условий — это рабство.

                                                          А вы точно с государством работали?
                                                          Ну можно же сначала собрать информацию. А если можно ее собрать, то зачем останавливать систему?

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

                                                          Повторюсь — узнать о рисках проще, чем узнать о технических подробностях и зависимостях. Я не пропагандирую этот метод как единственно-верный, но он не раз пролевал свет на сложные зависимости в БД и ПО.


                                                          1. lair
                                                            11.09.2015 22:18

                                                            Повторюсь, заказчику рефакторинг не нужен.

                                                            Значит, не надо его делать.

                                                            А вы точно с государством работали?

                                                            Без малого пять лет.

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

                                                            И как же вы, зная, что отключение БД влечет за собой риск срыва критического отчета, все-таки отключите ее, чтобы узнать, какие системы с БД связаны?


                                                            1. Delphinum
                                                              11.09.2015 22:20

                                                              Без малого пять лет.

                                                              И вы ставили условия государству?
                                                              И как же вы, зная, что отключение БД влечет за собой риск срыва критического отчета, все-таки отключите ее, чтобы узнать, какие системы с БД связаны?

                                                              Почему вы считаете, что я отключал БД под риском срыва критического отчета?


                                                              1. lair
                                                                11.09.2015 22:23

                                                                И вы ставили условия государству?

                                                                Я озвучивал условия выполнения задачи начальству, а те — заказчику. Не вижу в этом ничего странного.

                                                                Почему вы считаете, что я отключал БД под риском срыва критического отчета?

                                                                Я не считаю, я спрашиваю. Если не отключали — то как же вы выясните, какие системы с ней связаны?


                                                                1. Delphinum
                                                                  11.09.2015 22:27

                                                                  Я озвучивал условия выполнения задачи начальству, а те — заказчику. Не вижу в этом ничего странного.

                                                                  Мой опыт обычно сводился к следующему: есть бюджет, который пилится на 2 части, одна из которых идет в фирму, после чего еще немного пилится (ибо на фирме не только программисты живут), а остаток получает отдел. Озвучивать условия выполнения начальству конечно можно, но эти условия должны либо согласоваться с бюджетом, выделенным на проект (да, он выделяется заранее, а не на основании ваших условий), либо компания с этим заказчиком не работает.
                                                                  Если не отключали — то как же вы выясните, какие системы с ней связаны?

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


                                                                  1. lair
                                                                    11.09.2015 22:29

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

                                                                    Ну вот и прекрасный второй вариант.

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

                                                                    Прекрасно, мы выяснили, что риск высокий. Дальше что делать?


                                                                    1. Delphinum
                                                                      11.09.2015 22:32

                                                                      Ну вот и прекрасный второй вариант.

                                                                      Вам нельзя в продавцы )))
                                                                      Прекрасно, мы выяснили, что риск высокий. Дальше что делать?

                                                                      Однозначно не отключать БД. Использовать другие методы нахождения зависимостей.


                                                                      1. lair
                                                                        11.09.2015 22:36

                                                                        Использовать другие методы нахождения зависимостей.

                                                                        Какие?

                                                                        (собственно, с этого и надо было начинать)


                                                                        1. Delphinum
                                                                          11.09.2015 22:39

                                                                          (собственно, с этого и надо было начинать)

                                                                          Я понимаю что вам очень интересны другие методы анализа зависимостей (либо вы хотите, чтоб о них упомянули), но я начал с одного из методов (при чем самого действенного) и считаю, что его не следует списывать со счетов.
                                                                          Какие?

                                                                          Опрос, тестирование, анализ структуры, анализ логов и т.д.


                                                                          1. lair
                                                                            11.09.2015 22:42

                                                                            Опрос, тестирование, анализ структуры, анализ логов и т.д.

                                                                            Прекрасно. Так что мешает использовать их сразу, чтобы не провоцировать даунтайм в продуктиве?


                                                                            1. Delphinum
                                                                              12.09.2015 00:51

                                                                              Нежелание заказчика тратить время на то, что нужно мне, а не ему (кто работает с говнокодом?).


                                                                              1. lair
                                                                                12.09.2015 00:53

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


                                                                                1. Delphinum
                                                                                  12.09.2015 10:25
                                                                                  -1

                                                                                  Не хотите вы слушать собеседника, ну да ладно.


                  1. VolCh
                    13.09.2015 00:37
                    +1

                    Гораздо лучше эти несколько минут мониторить подключения к базе. А лучше подольше, хотя бы месяц — некоторые клиенты запускаются только раз в месяц максимум.


  1. ApeCoder
    10.09.2015 10:18

    Хотелось бы побольше узнать про миграции данных. Допустим у вас есть несколько клиентов на версиях 1.0 2.0 3.0
    1) Есть ли какой-нибудь способ миграции данных клиента 1.0 на 4.0 кроме последовательных миграций на промежуточные версии
    2) Если между соседними релизами было несколько последовательных зависимых друг от друга миграций, есть ли способ миграции клиента напрямую на релиз без прогона всего промежуточного. Есть ли инструменты, которые это облегчают?


    1. vorobyev_evgeney
      11.09.2015 11:13

      Это тема для отдельной статьи. Как соберусь — напишу :)


  1. roboter
    10.09.2015 14:40

    А что будет с городом который имеет одинаковое название но находится в разных странах?


    1. vorobyev_evgeney
      11.09.2015 11:18

      Согласен. Придется после генерации ручками вносить правки скрипт раз такое дело.


  1. igor_suhorukov
    10.09.2015 23:45

    liquibase имеет обширный инструмент рефакторинга БД


  1. gleb_l
    13.09.2015 22:35

    Перед тем, как что-либо трогать, полезно проверить programmability в БД на отсутствие ссылок на это что-то в тексте сторед-процедур (там может быть и динамика!), функций и триггеров — например, испустив следующее:

    select * from syscomments where text like '%<object_to_change>%'
    

    Ну и вообще, полезно знать, где и зачем используется динамика — для этого можно найти все EXEC и sp_execute_sql в текстах всех процедур и посмотреть, насколько хитро там формируются стейтменты — иногда может оказаться, что строки на исполнение берутся из полей таблиц ;)