Обычным примером для такого рода вещей является ведение таблицы для записи каждого события, которое приводит к точкам, а также агрегирование общего количества точек в столбце либо в пользовательской таблице, либо в какой-либо таблице статистики, которая содержит запись длякаждый пользователь.Это дает вам возможность вести учет каждого события, задним числом пересчитывать итоги, если вы изменяете, сколько стоит каждый тип события, и получать доступ к итогам (и использовать их в запросах) без необходимости каждый раз вычислять их.
Когда я делаю подобные вещи, я использую триггеры в базе данных, чтобы автоматически обновлять агрегированные итоги при сохранении событий, чтобы логика приложения оставалась незагроможденной.Вот пример, который работает в MySQL:
CREATE TABLE `users` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL,
`total_activity_points` int(11) UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `users_username_uk` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
INSERT INTO `users` (`username`) VALUES ('Bob');
INSERT INTO `users` (`username`) VALUES ('Alice');
CREATE TABLE `activities` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`title` varchar(20) NOT NULL,
`points` int(11) UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `activities_title_uk` (`title`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
INSERT INTO `activities` (`title`,`points`) VALUES ('Article', 10);
INSERT INTO `activities` (`title`,`points`) VALUES ('Vote', 5);
CREATE TABLE `user_activities` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` int(11) UNSIGNED NOT NULL,
`activity_id` int(11) UNSIGNED NOT NULL,
`create_date` DATETIME NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `user_activities_fk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
CONSTRAINT `user_activities_fk_2` FOREIGN KEY (`activity_id`) REFERENCES `activities` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
DELIMITER //
DROP TRIGGER IF EXISTS `after_insert_user_activities` //
CREATE TRIGGER `after_insert_user_activities`
AFTER INSERT
ON `user_activities`
FOR EACH ROW
BEGIN
DECLARE v_total INTEGER DEFAULT 0;
SET v_total = (SELECT SUM(`activities`.`points`)
FROM `user_activities`
INNER JOIN `activities` ON `activities`.`id`=`user_activities`.`activity_id`
WHERE `user_activities`.`user_id` = NEW.`user_id`);
UPDATE `users`
SET `total_activity_points` = v_total
WHERE `users`.`id` = NEW.`user_id`;
END;
//
DELIMITER ;
DELIMITER //
DROP TRIGGER IF EXISTS `after_delete_user_activities` //
CREATE TRIGGER `after_delete_user_activities`
AFTER DELETE
ON `user_activities`
FOR EACH ROW
BEGIN
DECLARE v_total INTEGER DEFAULT 0;
SET v_total = (SELECT SUM(`activities`.`points`)
FROM `user_activities`
INNER JOIN `activities` ON `activities`.`id`=`user_activities`.`activity_id`
WHERE `user_activities`.`user_id` = OLD.`user_id`);
UPDATE `users`
SET `total_activity_points` = v_total
WHERE `users`.`id` = OLD.`user_id`;
END;
//
DELIMITER ;
INSERT INTO `user_activities` (`user_id`,`activity_id`,`create_date`) VALUES (1,1,NOW());
INSERT INTO `user_activities` (`user_id`,`activity_id`,`create_date`) VALUES (1,2,NOW());
INSERT INTO `user_activities` (`user_id`,`activity_id`,`create_date`) VALUES (2,2,NOW());
INSERT INTO `user_activities` (`user_id`,`activity_id`,`create_date`) VALUES (2,2,NOW());
DELIMITER //
DROP TRIGGER IF EXISTS `after_update_activities` //
CREATE TRIGGER `after_update_activities` AFTER UPDATE ON `activities`
FOR EACH ROW
BEGIN
DECLARE v_finished INTEGER DEFAULT 0;
DECLARE v_userId INT(11) UNSIGNED;
DECLARE v_total INT(11) UNSIGNED;
DECLARE user_id_cursor CURSOR FOR SELECT DISTINCT(`user_activities`.`user_id`) FROM `user_activities` WHERE `user_activities`.`activity_id`=NEW.`id`;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_finished = 1;
IF NEW.`points` != OLD.`points` THEN
OPEN user_id_cursor;
get_user_ids: LOOP
FETCH user_id_cursor INTO v_userId;
IF v_finished = 1 THEN
LEAVE get_user_ids;
END IF;
-- recalculate and store scores
SET v_total = (SELECT SUM(`activities`.`points`)
FROM `user_activities`
INNER JOIN `activities` ON `activities`.`id`=`user_activities`.`activity_id`
WHERE `user_activities`.`user_id` = v_userId);
UPDATE `users`
SET `total_activity_points` = v_total
WHERE `users`.`id` = v_userId;
END LOOP get_user_ids;
CLOSE user_id_cursor;
END IF;
END;
//
DELIMITER ;
DELIMITER //
DROP TRIGGER IF EXISTS `before_delete_activities` //
CREATE TRIGGER `before_delete_activities` BEFORE DELETE ON `activities`
FOR EACH ROW
BEGIN
DECLARE v_finished INTEGER DEFAULT 0;
DECLARE v_userId INT(11) UNSIGNED;
DECLARE v_total INT(11) UNSIGNED;
DECLARE user_id_cursor CURSOR FOR SELECT DISTINCT(`user_activities`.`user_id`) FROM `user_activities` WHERE `user_activities`.`activity_id`=OLD.`id`;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_finished = 1;
OPEN user_id_cursor;
get_user_ids: LOOP
FETCH user_id_cursor INTO v_userId;
IF v_finished = 1 THEN
LEAVE get_user_ids;
END IF;
-- recalculate and store scores
SET v_total = (SELECT SUM(`activities`.`points`)
FROM `user_activities`
INNER JOIN `activities` ON `activities`.`id`=`user_activities`.`activity_id`
WHERE `user_activities`.`user_id` = v_userId
AND `user_activities`.`activity_id` != OLD.`id`);
UPDATE `users`
SET `total_activity_points` = v_total
WHERE `users`.`id` = v_userId;
END LOOP get_user_ids;
CLOSE user_id_cursor;
END;
//
DELIMITER ;
Триггеры в таблице действий обновляют итоги, когда значения точек изменяются или действия удаляются.Триггер до удаления необходим, поскольку MySQL не запускает триггеры удаления при каскадном удалении.