MySQL relationship schema - mysql

Assuming MySQL with a users table like
id | user_name | total_likes | updated_at
and a likes table like
id | user_id | like
What I need is to when the likes table gets updated/inserted/deleted then the users.updated_at associated with likes.user_id gets updated to the current date. Further when an insert happens to the likes table, then users.total_likes increases by 1 value.
I can do all the above with queries however, I am trying to use the power of relationships in MySQL. Please can you advise?

As David mentioned above, you are most likely looking for triggers. Something like this:
Delimiter //
CREATE TRIGGER ai_likes AFTER INSERT ON likes
FOR EACH ROW
BEGIN
INSERT INTO users (updated_at) VALUES
NOW()
WHERE id = NEW.id;
UPDATE users SET total_likes = (total_likes + 1)
WHERE id = NEW.id;
END//
Delimiter ;
CREATE TRIGGER au_likes AFTER UPDATE ON likes
FOR EACH ROW
INSERT INTO users (updated_at) VALUES
NOW()
WHERE id = NEW.id;
CREATE TRIGGER ad_likes AFTER DELETE ON likes
FOR EACH ROW
INSERT INTO users (updated_at) VALUES
NOW()
WHERE id = NEW.id;

Related

How to update related table MYSQL?

There are two tables: orders, orders_history.
orders
________
id | status
orders_history
id | order_id | status | user_id
The orders_history contains history of all user's actions. At the same time orders.status contains the last status from orders_history.status.
I make these queries in transation:
transaction start
insert into orders_history...
$status = select status from order_history order by id desc limit 1;
update orders set status = $status where orders.id = id
My question is:
Should I use transaction and is it properly way to do that?
What if several transactions try to insert, update orders_history for the same order_id.
As suggested in the comments above you could use a trigger to update the orders table -
DELIMITER $$
CREATE TRIGGER `update_order_status` AFTER INSERT ON `orders_history`
FOR EACH ROW
UPDATE `orders` SET `status` = NEW.status WHERE id = NEW.order_id;
$$
DELIMITER ;
The better option would be to not store the redundant status in orders and just query for most recent status in orders_history.
SELECT orders.id, (SELECT status FROM orders_history oh WHERE orders.id = oh.order_id ORDER BY id DESC LIMIT 1) AS status
FROM orders
The design pattern I might use in this case is...
Table 1: History -- this is an audit trail of everything that has gone on. (Think: All the checks written and deposits made to a checking account.)
Table 2: Current -- this is the current status of the information. (Think, current account balance, status, etc.)
Whenever something happens (eg, a check clears):
START TRANSACTION;
INSERT INTO History ...;
UPDATE Current ...;
COMMIT;
In the case of a checking account, something different is needed if your account is overdrawn, so let's make the transaction more complex:
START TRANSACTION;
SELECT balance FROM Current WHERE acct = 123 FOR UPDATE;
if would be overdrawn then
email user
UPDATE Current SET status = 'overdrawn' acct = 123;
...
else
INSERT INTO History ...;
UPDATE Current ...;
endif
COMMIT;
I prefer to put the "business logic" clearly in one place, not hidden in a Trigger. (I might use a Trigger for monitoring or logging, but not for the main purpose of the tables.)

Table trigger to indicate two records were merged

I have some simple triggers on a table which work for to audit changes to a record by logging to a table any time an INSERT, UPDATE, or DELETE occurs. When a record is inserted into Users, a copy is made in Users_History with a Status field populated with the value 'INSERT'. Same is done for UPDATE and DELETE.
DELIMITER $$
CREATE TRIGGER Users_History_Insert AFTER INSERT ON Audit
FOR EACH ROW BEGIN
INSERT INTO Users_History select *, 'INSERT' from Users where ID = NEW.ID;
END$$
DELIMITER ;
DELIMITER $$
CREATE TRIGGER Users_History_Delete BEFORE DELETE ON Audit
FOR EACH ROW BEGIN
INSERT INTO Users_History select *, 'DELETE' from Users where ID = OLD.ID;
END$$
DELIMITER ;
DELIMITER $$
CREATE TRIGGER Users_History_Update AFTER UPDATE ON Audit
FOR EACH ROW BEGIN
INSERT INTO Users_History select *, 'UPDATE' from Users where ID = NEW.ID;
END$$
DELIMITER ;
We have a function where two Users records can be merged with each other. Effectively, all values of one record are overwritten with the values of another record, and the unchanged record is deleted.
If you had a table such as this:
| ID | Name | email |
| 1 | Billy | bill#mail.com |
| 2 | Bill | bill2#mail.com |
And I wanted to merge record 2 into 1, I'd end up with this result.
| ID | Name | email |
| 1 | Bill | bill2#mail.com |
I'd like to create a trigger that would have this in Users_History where 2 is the record merged from.
| Users_ID | Name | Email | Status |
| 1 | Bill | bill2#mail.com | MERGED:2 |
As it stands now, we would end up with two records, an UPDATE and a DELETE
UPDATE `Users_To`
SET `Name` = `Users_From`.`Name`, `email` = `Users_From`.`email`
FROM `Users` AS `Users_To`
CROSS JOIN `Users` AS `Users_From`
WHERE `Users_To`.`ID` = 1 AND `Users_From`.`ID` = 2;
DELETE FROM `Users` WHERE `ID` = 2;
I can't think of a way to do this in SQL, is it possible? As I understand, the triggers have no knowledge of the JOIN or WHERE clause affecting a record.
Your UPDATE query has a flaw and is correctly
UPDATE `Users` AS `Users_To` CROSS JOIN `Users` AS `Users_From`
SET `Users_To`.`Name` = `Users_From`.`Name`, `Users_To`.`email` = `Users_From`.`email`
WHERE `Users_To`.`ID` = 1 AND `Users_From`.`ID` = 2;
DELETE FROM `Users` WHERE `ID` = 2;
What the trigger does is simple first it check if the update is a modify of an existing row by checking if there are mor than one rows with the same email.
If that the case grab the other id in your case 2 and add it ti the text.
This works as long you have only 2 rows involved in your update query.
And i don't know your your table, but if you have a UNIQUE constraint you will have a problem with the update.
DELIMITER $$
DROP TRIGGER IF EXISTS Users_History_Update;
CREATE TRIGGER Users_History_Update AFTER UPDATE ON users
FOR EACH ROW BEGIN
SELECT Count(*) INTO #number FROM Users WHERE `email` = NEW.`email`;
IF #number > 1 THEN
SELECT `ID` INTO #id FROM Users WHERE `email` = NEW.`email` AND `ID` <> NEW.`ID`;
INSERT INTO Users_History VALUES (OLD.`ID`,OLD.`Name` ,OLD.`email`, CONCAT('MERGED:',#id));
ELSE
INSERT INTO Users_History VALUES (OLD.`ID`,OLD.`Name` ,OLD.`email`, 'UPDATE');
END IF;
END$$
DELIMITER ;

Trigger to delete all but x rows per index

So I have two tables: posts and server_options.
posts consists of:
no
date_created
server_id
user_id
etc
server_options consists of:
no
posts_per_user
server_id
etc...
A user is only allowed server_options.posts_per_user posts per server. To limit this, I have a trigger executed before insert and update to posts:
CREATE DEFINER=`root`#`localhost` TRIGGER `bi_posts` BEFORE INSERT ON `posts` FOR EACH ROW BEGIN
SELECT posts_per_user INTO #posts_per_user FROM `server_options` WHERE server_id = NEW.server_id LIMIT 1;
SELECT COUNT(0) INTO #post_count FROM `posts` WHERE server_id = NEW.server_id AND user_id = NEW.user_id;
IF #post_count >= #posts_per_user
THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Cannot add or update row: post limit exceeded.';
END IF;
END
Simple enough. However, what I'd like to do is also add a trigger to server_options on update. If posts_per_user changes, it should remove any "excess" posts for that server from posts.
For example. If some users have 5 posts in a server, and the posts_per_user is reduced to 3, it should delete the oldest and keep only 3 for that user for that server.
Any pointers on where to begin? no is AI PK, so we can sort by that rather than date_added to make things easier.
I was thinking this (it works), but to me it seems like a bad approach:
CREATE DEFINER=`root`#`localhost` TRIGGER `au_server_options` AFTER UPDATE ON `server_options` FOR EACH ROW BEGIN
IF NEW.posts_per_user < OLD.posts_per_user
THEN
DELETE FROM `posts`
WHERE NEW.posts_per_user <= (
SELECT count(0)
FROM (SELECT no, server_id, user_id FROM posts) AS posts_temp
WHERE
`posts`.`server_id` = `posts_temp`.`server_id` AND
`posts`.`user_id` = `posts_temp`.`user_id` AND
`posts`.`no` > `posts_temp`.`no`
ORDER BY `posts`.`no`
);
END IF;
END
I'm not looking at huge amounts of entries. Maybe a 100,000 or so.
Thanks, y'all!

Update field with another auto increment field value MySQL

I have a table in MYSQL database with two fields:
Id (auto increment field).
Post_Id.
When I insert a new record both fields should have the same value. So I should update post_id with Id value, and at the same time make sure that I update the field with the right value not with any other new inserted record value.
I tried this SQL statement but it was very slow and I was not sure that I select the right value:
set #auto_id := (SELECT AUTO_INCREMENT
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME='table_name'
AND TABLE_SCHEMA=DATABASE() );
update table_name set post_id= #auto_id where id=#auto_id ;
I don't have long experience with MySQL and I cannot change the table structure .
The approach you followed is not transaction safe as well.
The best option I can think about is to use trigger
Edit: According to #lagripe's mentionings
CREATE TRIGGER sometrigger
AFTER INSERT ON sometable
BEGIN
SET NEW.post_id := (SELECT id from sometable order by DESC limit 1) + 1 ; // you may need +1 here. I couldn't test it.
END
or you may consider to use LAST_INSERT_ID
insert into table_name values ( .... );
update table_name set post_id = LAST_INSERT_ID();
but why do you need two columns with the same id at first place?
if you really need why don't you use computed/generated columns?
CREATE TABLE Table1(
id DOUBLE,
post_id DOUBLE as (id)
);
you can use triggers :
CREATE TRIGGER UpdatePOST_id
BEFORE INSERT ON table_db
FOR EACH ROW
SET NEW.post_id := (select id from table_db order by id DESC LIMIT 1)+1 ;
from now on, whatever you insert your as a value in post_id column will be replaced with the id inserted automatically.
Test :
|id|post_id|
|20| 20 |
|21| 21 |
|22| 22 |
|23| 23 |
To drop the trigger :
DROP trigger UpdatePOST_id

Make trigger update the rows I want

I am struggling with a trigger to update a second table based on the first table being updated.
I have tried this:
DELIMITER //
FOR EACH ROW
BEGIN
Update phpfb_picks
set points = NEW.value
where username = user and gameid = gameid
END;
//
delimiter;
but get an error about syntax.
If I do:
Update phpfb_picks
set points = NEW.value
where username = user and gameid = gameid
This works to a point. It updates all users records with the same value that was updated.
What I want to do is, when a value is updated on Table A, I want to update Table B with all records for that user, basically the 'value' from table A would always be pushed to 'points' of table B for the user, regardless if the actual record was updated.
If any record for user is updated, update all of table B records with the same value based on the username and gameid
So if Table A has the following records:
username - test
gameid - 1
value - 1
username - test
gameid - 2
value - 2
when value from row 1 is updated to 3, i want to update Table B with all the current values from table A for that user.
Is this even possible?
UPDATE:
Table A (allpoints) has columns:
username
gameid
value
TABLE B (phpfb_picks) has columns:
user
gameid
points
allpoints.username = phpfb_picks.user
allpoints.gameid = phpfb_picks.gameid
allpoints.value = phpfb_picks.points
Whenever an update is made to allpoints, I want all the records for that specific user to update all the specific records for that user in phpfb_picks, passing the allpoints.value to phpfb_picks.points based on the user and gameid
Try this trigger:
DELIMITER $$
CREATE TRIGGER update_phpfb_picks
AFTER UPDATE ON allpoints FOR EACH ROW
BEGIN
UPDATE phpfb_picks
INNER JOIN allpoints ON allpoints.username = phpfb_picks.username AND
allpoints.gameid = phpfb_picks.gameid
SET phpfb_picks.points = allpoints.value
WHERE phpfb_picks.username = NEW.username;
END;
$$
DELIMITER ;
For each row that is updated in allpoints, all rows in phpfb_picks with the same username of the updated row will be updated to the respective value of points present in allpoints as value. The trigger is an AFTER UPDATE so that the newly updated value in allpoints will also be set in phpfb_picks.
Maybe this
drop trigger if exists phpfb;
DELIMITER $$
create trigger phpfb after update on allpoints
FOR EACH ROW
BEGIN
Update phpfb_picks
set points = NEW.value
where user = new.username and gameid = new.gameid;
END $$
delimiter ;
for example
drop table if exists allpoints,phpfb_picks;
create table allpoints (username char(4),gameid int, value int);
create table phpfb_picks (user char(4),gameid int, points int);
insert into allpoints (username,gameid) values
('test',1),('test',2);
insert into phpfb_picks (user,gameid) values
('test',1),('test',1),('test',2);
update allpoints
set value = 2 where username = 'test' and gameid = 1;
result
+------+--------+--------+
| user | gameid | points |
+------+--------+--------+
| test | 1 | 2 |
| test | 1 | 2 |
| test | 2 | 0 |
+------+--------+--------+
3 rows in set (0.00 sec)