Яндекс.Метрика

    Песочница

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

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

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

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

    1. уникальный номер
    2. дата и время создания
    3. дата и время оплаты
    4. юридическое лицо (контрагент) которому была произведена оплата
    5. сумма
    6. тип платежа

    Сразу отмечу что почти никогда не стоит связывать уникальный номер документа с id записи в таблице БД. Дело тут вот в чем: пользователи имеют свойство ошибаться. Логика пользователей иногда весьма своеобразна, к примеру, периодически им легче удалить неправильно созданный документ и создать его по новой (особенно это касается новых сотрудников, которые боятся “накосячить”). После подобных действий у вас в системе будет множество пустых “дыр” между номерами документов. Казалось бы мелочь, но любая проверка со стороны “заинтересованных” лиц найдет в этом тайный умысел. (Если хотите реальных примеров, погуглите “Прайм-ТАСС подало в суд на мэрию Москвы”. Вся их доказательная база — номера документов шли по порядку, но в общественном доступе есть только часть из них). Лучше всего, по нашему мнению, вновь создаваемому документу присваивать номер = максимальный уникальный номер в системе по данному типу документов + 1.
    Но вернемся к нашему примеру. Обратим внимание на поле №4 — юридическое лицо. Коль скоро у нас имеется справочник юридических лиц, то очевидным является записывать в это поле ссылку на запись в этом справочнике.
    А теперь представим себе несколько возможных ситуаций:
    1) Платежное поручение было создано в 2009 году. Платеж был произведен ООО “Кровать”. В январе 2010 фирма была переименована в ООО “Стулья”. Получается что если мы откроем форму платежного поручения в конце 2010 года, то увидим что платеж был произведен фирме, которой в 2009 году физически не существовало.
    2) Допустим в марте 2010 года произошло слияние ООО “Кровать” и ЗАО “Диваны”, результатом стало ОАО “Диваны и кровати”. Что могут сделать пользователи? А они могут переименовать фирму ООО “Кровать” в ОАО “Диваны и кровати”, а еще они могут переименовать ЗАО “Диваны” в “Диваныи кровати”. Самое интересное начнется при первом же отчете, когда окажется что все платежи между разным юридическим лицами (3мя фактическими и 4мя в базе) перемешались и отличить их могут только люди, которые производили оплату (которые, к сожалению, попали под сокращение штата и уже несколько месяцев не работают в вашей фирме).

    Конечно все действия можно восстановить по истории. Только заниматься этим будете вы, а не сотрудники бухгалтерии (ну не будут же они ковыряться во внутренностях вашей системы, в конце концов у них еще куча бумажек, которые надо разгрести; да и, в конце концов, кто разрабатывал систему? Ваш отдел? Ну так значит и проблема ваша. У бухгалтеров вообще работа — оплачивать, а не с системами разбираться).

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

    Итак, я подошел к тому о чем хотел рассказать.

    Проектирование сущностей, относящихся к документам.


    Выдвинем несколько требований:
    1. Все данные документа должны быть стойкими к изменениям. Это не значит что если пользователь захочет в платежном поручении изменить получателя платежа, то он не сможет этого сделать, но это значит что если он переименует получателя платежа в справочнике юридических лиц, то в платежом поручении отразится старое наименование.
    2. Документы должны выдерживать любую логику, описанную в бизнес-процессе. В описанном примере это означает, что если вам необходимо будет просуммировать суммы всех платежных поручений по всем юридическим лицам, у которых разное наименование — у вас будет для этого техническая возможность.

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

    Как мы решили эту проблему

    Мы рассматривали два варианта:

    1. В справочнике юридических лиц хранятся все изменения. Т.е. при обновлении данных в справочнике мы не просто записываем историю изменения, но и физически не обновляем изменяемую строку — мы просто помечаем ее как неактивную. При этом создается новая строка с флагом “актуальная запись”. При этом во вновь записанной строке создается ссылка на родительскую запись, т.е. на предыдущую версию данной строки. Так же при создании новой версии строки фиксируется время ее создания.
    Таким образом зная дату создания документа (в примере платежного поручения) мы всегда можем выяснить какие данные по контрагенту были на тот момент актуальными.
    Более того, коль скоро у нас версии записи имеют разные id в таблице, суммировать (группировать/классифицировать/разделять) по ним — одно удовольствие.

    2. Справочник юридических лиц состоит из двух таблиц.
    Первая содержит только те поля, которые характеризуют данную сущность и называется основной. Обычно это неизменяемые поля данной сущности (в примере с фирмами скорее всего должна подойти пара ИНН и КПП). Замечу что вполне может быть так, что таких полей будет ноль, т.е. единственным неизменяемым полем является id в таблице, и это абсолютно нормально.
    Вторая же содержит все поля данной сущности, а также ссылку на запись в родительской таблице и время создания новой записи.
    Таким образом все документы ссылающиеся на данное юридическое лицо будут хранить одну ссылку (на запись в основной таблице), а вот та версия, которая является актуальной для данного документа легко восстанавливается по времени создания документа и по колонке “время модификации” записи в подчиненной таблице справочника.

    В итоге был выбран второй вариант. И вот почему:
    1. Общая ссылка на родительский пункт оставляет все преимущества первого варианта (мы все так же можем легко разделить все варианты записи на их временные модификации и работать с каждой по отдельности), но и работа со всеми версиями родительской записи не вызывает никаких трудностей.
    2. MS SQL: так как в первом случае у нас следующая модификация ссылается на предыдущую (т.е. NULL < — вер1 < — вер2 < — вер3 < — … < — верN), то
    а) для того чтобы просмотреть всю историю изменений нам прийдется обойти цепь с i-того узла влево и вправо, на что на уровне базы данных потребуются какие-то затраты. А это атомарная функция, которая всего лишь возвращает информацию об изменениях записи.
    б) не получится использовать кластеризацию по версиям (во втором случае очень удобно навесить индекс на ссылку на запись в родительской таблице). Таким образом мы очень дешево (с точки зрения БД) и быстро сможем получать информацию по всем модификациям записи.
    3. Второй вариант позволяет на уровне базы данных очень просто запретить изменять данные, содержащиеся в основной таблице. Таким образом при неудачной попытке их модифицировать, пользователь хотя бы задумается о правильном пути решения своей проблемы (в крайнем случае обратиться к человеку, сопровождающему программу).

    На этом думаю хватит для второй части.
    С удовольствием отвечу на все ваши вопросы.