Перейти к основному содержимому

Нумерация в системе

Документы и сущности в системе:

  • Договор с контрагентом
  • Заказ покупателя
  • Заказ на производство
  • Спецификация к заказу
  • Счёт на оплату
  • Универсальный передаточный документ
  • Товарная накладная
  • Товарно-транспортная накладная
  • Счёт-фактура
  • Акт выполненных работ
  • Складская учётная единица
  • Номер задачи

Договор с контрагентом

Описание Нумерация: код контрагента + дата договора + номер по порядку (ОМ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
Управление ресурсамиПроще — одна таблицаТребует управления последовательностями
Гибкость / контрольБольше ручных настроекМеньше возможностей для настройки
Подходит дляНебольшого и среднего масштабаОчень большого масштаба, требующего высокой нагрузки

Вывод

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

Так как конкуренция минимальная, то реализовывам первый варинт