Нумерация в системе
Документы и сущности в системе:
- Договор с контрагентом
- Заказ покупателя
- Заказ на производство
- Спецификация к заказу
- Счёт на оплату
- Универсальный передаточный документ
- Товарная накладная
- Товарно-транспортная накладная
- Счёт-фактура
- Акт выполненных работ
- Складская учётная единица
- Номер задачи
Договор с контрагентом
Описание
Нумерация: код контрагента + дата договора + номер по порядку (ОМ250609-11)
Заказ покупателя
Описание Нумерация: единый номер по порядку
Заказ на производство
Описание
Нумерация: номер заказа покупателя - номер товара по порядку в заказе (11-2)
Спецификация к заказу покупателя
Описание Нумерация: единый номер по порядку
Счёт на оплату
Описание
Нумерация: год + номер по порядку для выбранного нашего контрагента (25-1). Каждый год начинается с 1.
Универсальный передаточный документ, Товарная накладная, Товарно-транспортная накладная
Описание Нумерация: не ведётся отдельно. Соответствует номеру связанной счёт фактуры Единый номер для Универсального передаточного документа, Товарной накладной, Товарно-транспортной накладной, Счёт-фактуры
Акт выполненных работ
Описание Нумерация: не ведётся отдельно. Соответствует номеру связанной счёт фактуры Единый номер для Акт выполненных работ, Счёт-фактуры для акта
Счёт-фактура
Описание
Нумерация: номер по порядку для выбранного нашего контрагента (17). Каждый год начинается с 1.
Складская учётная единица
Описание Нумерация(код): единый номер по порядку (17)
Номер задачи
Описание Нумерация: единый номер по порядку (17)
Рассматриваемые варианты реализации
для нумераций номер по порядку для выбранного нашего контрагента
1. Использование таблицы с обновлением и вставкой
Описание 1
- Хранит счетчики в отдельной таблице (
document_counters) - Обновление через
UPDATE ... RETURNINGс транзакциями и попытками вставки при отсутствующей записи
Преимущества 1
- Универсальный и гибкий — легко добавлять новые комбинации
- Нет необходимости создавать множество последовательностей
- Легко управлять и восстанавливать счетчики
Недостатки 1
- Более сложная логика при конкуренции (нужно делать повторные попытки)
- Возможна повышенная нагрузка на таблицу счетчиков
- Реализация на транзакциях, чуть более сложная реализация
Примерная реализация
CREATE TABLE document_counters (
organization_id INT NOT NULL,
document_type_id INT NOT NULL,
year INT NOT NULL,
current_number INT NOT NULL,
PRIMARY KEY (organization_id, document_type_id, year)
);
using Npgsql;
using System;
public class DocumentNumberService
{
private readonly string _connectionString;
public DocumentNumberService(string connectionString)
{
_connectionString = connectionString;
}
public int GetNextDocumentNumber(int organizationId, int documentTypeId, int year)
{
using var conn = new NpgsqlConnection(_connectionString);
conn.Open();
while (true)
{
using var transaction = conn.BeginTransaction();
// Пытаемся увеличить и вернуть счётчик
using (var cmdUpdate = new NpgsqlCommand(@"
UPDATE document_counters
SET current_number = current_number + 1
WHERE organization_id = @orgId AND document_type_id = @docTypeId AND year = @year
RETURNING current_number", conn, transaction))
{
cmdUpdate.Parameters.AddWithValue("orgId", organizationId);
cmdUpdate.Parameters.AddWithValue("docTypeId", documentTypeId);
cmdUpdate.Parameters.AddWithValue("year", year);
var result = cmdUpdate.ExecuteScalar();
if (result != null)
{
transaction.Commit();
return Convert.ToInt32(result);
}
}
// Если записи нет — пытаемся вставить с current_number = 1
try
{
using var cmdInsert = new NpgsqlCommand(@"
INSERT INTO document_counters (organization_id, document_type_id, year, current_number)
VALUES (@orgId, @docTypeId, @year, 1)", conn, transaction);
cmdInsert.Parameters.AddWithValue("orgId", organizationId);
cmdInsert.Parameters.AddWithValue("docTypeId", documentTypeId);
cmdInsert.Parameters.AddWithValue("year", year);
cmdInsert.ExecuteNonQuery();
transaction.Commit();
return 1;
}
catch (PostgresException ex) when (ex.SqlState == "23505")
{
// Конфликт при вставке — повторяем попытку
transaction.Rollback();
}
catch
{
transaction.Rollback();
throw;
}
}
}
}```
</details>
---
### 2. Использование последовательностей (sequences)
#### Описание 2
- Для каждой `(org_id, doc_type_id, year)` создается отдельная последовательность
- Получение номера — вызов `nextval()`, которая потокобезопасна и атомарна
#### Преимущества 2
- Высокая потокобезопасность и производительность — `sequence` — встроенный потокобезопасный механизм
- Простая реализация: создал — вызывай `nextval()`
- Не требуется сложных транзакций и повторных попыток
- Хорошо подходит при большом числе уникальных номеров
#### Недостатки 2
- Множество последовательностей — может привести к значительному росту базы данных
- Управление — нужно создавать и удалять последовательности по мере необходимости
- Менее гибкий — тяжеловато контролировать "записные" номера, если нужно фиксировать их ещё в ходе вставки
<details>
<summary>Примерная реализация</summary>
```.net
using Npgsql;
using System;
public class DocumentNumberService
{
private readonly string _connectionString;
public DocumentNumberService(string connectionString)
{
_connectionString = connectionString;
}
public int GetNextDocumentNumber(int organizationId, int documentTypeId, int year)
{
string sequenceName = $"sequence_{organizationId}_{documentTypeId}_{year}";
using var conn = new NpgsqlConnection(_connectionString);
conn.Open();
// Проверка и создание последовательности при необходимости
EnsureSequenceExists(conn, sequenceName);
// Получение следующего номера из последовательности
int nextNumber = GetNextval(conn, sequenceName);
return nextNumber;
}
private void EnsureSequenceExists(NpgsqlConnection conn, string sequenceName)
{
string createSequenceSql = $@"
DO
$$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_class WHERE relkind = 'S' AND relname = '{sequenceName}'
) THEN
EXECUTE 'CREATE SEQUENCE {sequenceName} START WITH 1 INCREMENT BY 1';
END IF;
END
$$
";
using var cmd = new NpgsqlCommand(createSequenceSql, conn);
cmd.ExecuteNonQuery();
}
private int GetNextval(NpgsqlConnection conn, string sequenceName)
{
using var cmd = new NpgsqlCommand($"SELECT nextval('{sequenceName}')", conn);
var result = cmd.ExecuteScalar();
return Convert.ToInt32(result);
}
}
Итоговое сравнение
| Критерий | Таблицы с обновлением | Последовательности |
|---|---|---|
| Простота реализации | Средняя | Высокая |
| Производительность | Хорошая при небольшом числе комбинаций | Отличная, потокобезопасная |
| Масштабируемость | Может замедляться при росте таблицы | Хорошая, но нужно управлять большим числом seq |
| Управление ресурсами | Проще — одна таблица | Требует управления последовательностями |
| Гибкость / контроль | Больше ручных настроек | Меньше возможностей для настройки |
| Подходит для | Небольшого и среднего масштаба | Очень большого масштаба, требующего высокой нагрузки |
Вывод
- Таблица с обновлением — более универсальна, подходит, если нужно управлять счетчиками гибко и с меньшими затратами.
- Последовательности — оптимальны при высокой частоте вызова, необходимости быстрого получения номера и меньших накладных расходов при высокой конкуренции.
Так как конкуренция минимальная, то реализовывам первый варинт