#php #mysql #multi-tenant #invoice
Вопрос:
Я нашел два разных способа, во-первых, получить следующий номер счета-фактуры, а затем сохранить счет-фактуру в базе данных с несколькими арендаторами, где, конечно, у каждого арендатора будут свои собственные счета-фактуры с разными добавочными номерами.
- Мой первый (и фактический) подход таков (отлично работает):
- Добавьте новую запись в таблицы счетов-фактур. Независимо от номера счета-фактуры (например, 0 или пустой)
- Я получаю уникальный идентификатор ЭТОЙ созданной записи после вставки
- Теперь я делаю
"SELECT table where ID = $lastcreatedID **FOR UPDATE**"
- Здесь я получаю последний сохраненный номер счета с
"SELECT @A:=MAX(NUMBER) 1 FROM TABLE WHERE......"
- Наконец, я обновляю ранее сохраненную запись с этим номером счета с помощью
"UPDATE table SET NUMBER = $mynumber WHERE ID = $lastcreatedID"
Это работает нормально, но я не знаю, действительно ли требуется «для обновления» или это правильный способ сделать это в многопользовательской базе данных из-за производительности и т. Д.
- Второй (и более простой) подход заключается в следующем (и тоже работает, но я не знаю, является ли это безопасным подходом):
INSERT INTO table (NUMBER,TENANT) SELECT COALESCE(MAX(NUMBER),0) 1,$tenant FROM table WHERE....
- Вот и все
Оба метода работают, но я хотел бы знать различия между ними в отношении скорости, производительности, может ли это создавать дубликаты и т.д.
Или… есть ли лучший способ сделать это?
Я использую MySQL и PHP. Приложение представляет собой облачное программное обеспечение для выставления счетов/продаж, которое будет использоваться многими клиентами (арендаторами).
Спасибо
Комментарии:
1. «Хорошо, я добавил счет. Упс, там была какая-то плохая информация, поэтому я ее удалил. Хорошо, я сделал новый счет с правильной информацией. Подождите, у него тот же номер счета, что и у предыдущего? И есть куча плохих метаданных из предыдущего сломанного счета-фактуры? О, никакие части приложения просто не сломаны…» Это то, что ждет вас, когда вы назначаете идентификаторы
MAX() 1
. Есть дополнительный уровень плохого, если учесть, что это мультитенант, и есть большая вероятность, что ошибка попадет в совершенно другого арендатора.2. Я не назначаю идентификаторы с максимальным значением 1. Я присваиваю номера счетов-фактур. Это не удостоверение личности. Вы не должны удалять счет-фактуру. Вы должны (по крайней мере, в Испании) создать новый, исправляющий плохой. В любом случае… если вы удалите счет-фактуру, следующий номер счета-фактуры будет max (счет-фактура) 1. Опять же, я думаю, что вы перепутали идентификатор (уникальный для каждой записи) с номером счета.
Ответ №1:
Независимо от того, используете ли вы эти значения в качестве идентификаторов базы данных или нет, повторное использование идентификаторов практически гарантированно вызовет проблемы в какой-то момент. Даже если вы не используете повторно идентификаторы, вы столкнетесь со случаем, когда два запроса на создание счета выполняются одновременно и получают один и тот же MAX() 1
результат.
Чтобы обойти все это, вам нужно переопределить простой генератор последовательностей, который блокирует его хранилище во время выдачи значения. Напр.:
CREATE TABLE client_invoice_serial (
-- note: also FK this back to the client record
client_id INTEGER UNSIGNED NOT NULL PRIMARY KEY,
serial INTEGER UNSIGNED NOT NULL DEFAULT 0
);
$dbh = new PDO('mysql:...');
/* this defaults to 'on', making every query an implicit transaction. it needs to
be off for this. you may or may not want to set this globally, or just turn it off
before this, and back on at the end. */
$dbh->setAttribute(PDO::ATTR_AUTOCOMMIT,0);
// simple best practice, ensures that SQL errors MUST be dealt with. is assumed to be enabled for the below try/catch.
$dbh->setAttribute(PDO::ATTR_ERRMODE_EXCEPTION,1);
$dbh->beginTransaction();
try {
// the below will lock the selected row
$select = $dbh->prepare("SELECT * FROM client_invoice_serial WHERE client_id = ? FOR UPDATE;");
$select->execute([$client_id]);
if( $select->rowCount() === 0 ) {
$insert = $dbh->prepare("INSERT INTO client_invoice_serial (client_id, serial) VALUES (?, 1);");
$insert->execute([$client_id]);
$invoice_id = 1;
} else {
$invoice_id = $select->fetch(PDO::FETCH_ASSOC)['serial'] 1;
$update = $dbh->prepare("UPDATE client_invoice_serial SET serial = serial 1 WHERE client_id = ?");
$update->execute([$client_id])
}
$dbh->commit();
} catch(PDOException $e) {
// make sure that the transaction is cleaned up ASAP, then let the exception bubble up into your general error handling.
$dbh->rollback();
throw $e; // or throw a more pertinent error/exception of your choosing.
}
// both committing and rolling back will release the lock
На самом базовом уровне это то, что MySQL делает в фоновом режиме для столбцов АВТОИНКРЕМЕНТА.
Комментарии:
1. Спасибо, @Sammitch. Таким образом, кажется, что первый подход (использование «для обновления») в транзакции-лучший способ достичь этого. Я использую mysqli_ вместо PDO, поэтому, боюсь, мне нужно кое — что изменить.
2. Все вызовы PDO должны иметь эквиваленты MySQLi, и, как вы сказали, настоящая магия заключается
SELECT ... FOR UPDATE; INSERT/UPDATE
в транзакции. 😉
Ответ №2:
Не используйте MAX(id) 1
. Когда-нибудь он тебя укусит. Там будет два счета с одинаковым номером, и нам потребуется несколько абзацев, чтобы объяснить, почему это произошло.
Вместо этого используйте AUTO_INCREMENT
так, как оно предназначено.
INSERT INTO Invoices (id, ...) VALUES (NULL, ...);
SELECT LAST_INSERT_ID(); -- specific to the conne ction
Это безопасно, даже если несколько соединений выполняют одно и то же. Нет FOR UPDATE
, нет BEGIN
и т. Д. Необходимо. (Возможно, вам это понадобится для других целей.)
И никогда не удаляйте строки. Вместо этого используйте стандартную деловую практику признания недействительными неверных счетов-фактур. Представьте, что вас проверяют.
Все это говорит о том, что потенциальная проблема все еще существует. После сбоя ROLLBACK
или сбоя системы идентификатор может быть «сожжен». Также такие вещи, как INSERT IGNORE
выделение идентификатора, прежде чем проверять, понадобится ли он.
Если вы можете смириться с предостережениями, используйте AUTO_INCREMENT
.
Если нет, то создайте таблицу из 1 строки и 2 столбцов для имитации генератора порядковых номеров: http://mysql.rjweb.org/doc.php/index_cookbook_mysql#sequence
Или используйте MariaDB SEQUENCE
Комментарии:
1. Насколько я понимаю, проблема в том, что это что-то вроде биллинговой системы SaaS. Существует одна
Invoice
таблица, содержащая счета-фактуры, созданные несколькими объектами пользователей SaaS, и, по-видимому, OP хочет, чтобы у каждого пользователя SaaS были последовательные идентификаторы счетов-фактур. Мне любопытно, есть ли у вас какие-либо мысли по поводу решения, которое я опубликовал. [Также до тех пор, пока у mariadb теперь не появятся генераторы последовательностей]2. @Sammitch — Неожиданные пробелы; откат; сбой; атомарность выборки нового номера по сравнению с его сохранением. Вам нужны отдельные номера для каждого арендатора или одна последовательность для всех? Производительность вряд ли будет проблемой. Задействована ли репликация/кластеризация? И т.д.
3. Включение генерации идентификатора в транзакцию
SELECT ... FOR UPDATE
должно решить эту первоначальную проблему. Другие вопросы лучше всего направлять в OP для уточнения, но я предполагаю, что OP хочет, чтобы у каждого пользователя была своя последовательность идентификаторов счетов-фактур.
Ответ №3:
Оба подхода действительно работают, но каждый из них имеет свои недостатки в условиях интенсивного движения.
Первый подход запускает 3 запроса для каждого создаваемого вами счета, что создает дополнительную нагрузку на ваш сервер.
Второй подход может привести к дубликатам в событиях, когда два счета-фактуры создаются с очень небольшой разницей во времени (например, запрос SELECT возвращает одинаковое максимальное число для обоих счетов-фактур).
Оба подхода могут привести к проблемам в условиях интенсивного движения.
Ниже перечислены два решения этих проблем:
- Используйте сгенерированные столбцы: Mysql поддерживает сгенерированные столбцы, которые в основном получены с использованием других значений столбцов для каждой строки. Обратитесь к этому
- Рассчитать номер счета на лету: Поскольку вы используете первичный ключ как часть счета-фактуры, позвольте базе данных обрабатывать создание уникальных первичных ключей, а затем генерировать номера счетов-фактур на лету в вашей бизнес-логике, используя идентификатор для каждого счета-фактуры.
Комментарии:
1. Привет, @Aayush. Позволяет ли сгенерированный столбец использовать «max ()» для получения последнего номера счета для ЭТОГО арендатора? Помните, что столбец НОМЕР не является столбцом идентификатора (уникальным).