RELATIONSHIP : students (1 can have N) addresses
SCENARIO: Students can have many records but only one associated record must have 'current' field set as 'Yes' (other value is NULL) so the query below should always return only one record per student.
SELECT * FROM address WHERE student_id = 5 AND current = 'Yes'
PROBLEM:
People sometimes mark more than one record as 'Yes' after INSERT or UPDATE for same student so I need to avoid it. What is the best way of doing it by using triggers or stored procedures within MySQL?
If UPDATE happens on 'address' table then this should run somewhere to mark other records as NULL: UPDATE addresses SET current = NULL WHERE student_id = IN_student_id
If INSERT happens on 'address' table then this should run somewhere to mark other records as NULL: UPDATE addresses SET current = NULL WHERE student_id = IN_student_id AND id <> IN_inserted_id
Thanks in advance
If you need something updated automatically after data is modified, the right approach is a trigger. Notice a trigger may call a stored procedure.
However you will not be able to implement the described behaviour in a trigger because:
A stored function or trigger cannot modify a table that is already being used (for reading or writing) by the statement that invoked the function or trigger.
In fact, the information "Address X is the current address" should be stored in a column in the students table, as a foreign key to the address table. Therefore, unicity is guaranteed.
Something like this (fiddle with it here):
CREATE TABLE student (
id INT NOT NULL PRIMARY KEY,
current_address INT,
name VARCHAR(20)
);
CREATE TABLE address (
id INT NOT NULL PRIMARY KEY,
student_id INT NOT NULL,
contents VARCHAR(50) NOT NULL,
CONSTRAINT student_fk FOREIGN KEY student_fk_idx (student_id)
REFERENCES student(id)
);
ALTER TABLE student
ADD CONSTRAINT curraddr_fk_idx
FOREIGN KEY curraddr_fk_idx (id, current_address)
REFERENCES address(student_id, id);
Notice this structure allows insertion of students with no "current address". This is because at least one for the two tables must allow a NULL value for their foreign key (or else we cannot add a single row in either table). If it makes more sense, let address.student_id be NULL instead, and allow an address to be nobody's address until you create the corresponding student.
Related
I have 2 tables in a database, users and users_removed with columns "id(primary key), email(unique), password" and "id, user_id(foreign key (user_id) references users(id)" respectively.
When a user registers the users table gets the data accordingly. And when the user wants to delete account I can get user's id in users_removed and consider it deleted such as
INSERT into users_removed (user_id)
VALUES ((SELECT id FROM users WHERE email = 'user#example.com'))
The id from users gets inserted into users_removed with a foreign key constraint.
Now the question is what will be the right way to get rid of data from users with that id but preserve it somehow.
Deleting entirely is not an option because I loose data and so the purpose of the table users_removed. Also if I delete I get error "Cannot delete or update a parent row: a foreign key constraint fails" because of the foreign key constraint.
The user should be able to re-register with previous email but considering it an entirely new entry, as email in users is unique.
Is there a way in sql to make certain data unable to be used, disallow to perform query on it, such as it gets ignored when I perform query in the backend.
Or what could be the possible ways to the solution?
I have a way of restricting users_removed to be able to login, but how should I proceed with the registration thing.
As mysql doesn't allow rerecly to use a select in the INSERT and delete from the same table, you must corcumvent ideally in a programming language out side of mysql.
I used here a seperate SELCT with a user defined variable, to get first the user_id
CREATE TABLE users (id int AUTO_INCREMENT PRIMARY key, email varchar(100) UNIQUE)
CREATE TABLE users_removed (id int AUTO_INCREMENT PRIMARY key,user_id int)
INSERT INTO users (email) VALUES ('user#example.com')
CREATE TRIGGER after_users_removed_insert
AFTER INSERT
ON users_removed FOR EACH ROW
BEGIN
IF NEW.user_id IS NOT NULL THEN
DELETE FROM users WHERE id = new.user_id;
END IF;
END
SELECT id INTO #user_id FROM users WHERE email = 'user#example.com' ;
INSERT into users_removed (user_id)
VALUES (#user_id)
SELECT * FROM users
id
email
INSERT INTO users (email) VALUES ('user#example.com')
SELECT * FROM users
id
email
2
user#example.com
fiddle
IMHO it will better to add two fields (IsDeleted, DeletedAt) to the users table.
CREATE TABLE usersss (
id int unsigned NOT NULL auto_increment,
email varchar(100),
IsDeleted tinyInt default 0,
DeletedAt datetime,
PRIMARY KEY (id),
UNIQUE KEY uk_email (email, IsDeleted),
KEY email (email),
KEY IsDeleted (IsDeleted)
);
You will include (IsDeleted=0) within your conditions in every query that deals with users.
when the user wants to delete account, You will set IsDeleted to 1 and DeletedAt to NOW().
UPDATE users SET IsDeleted=1 AND DeletedAt = NOW() WHERE id=$ID;
To make user able to re-register with previous email you will check for a unique index on fields (email, IsDeleted) not on (email).
SELECT COUNT(id) FROM users WHERE (IsDeleted=0) AND (email = '$email');
If query returns (0) then the user can use that email.
you may remove the data of users with IsDeleted=1 after a specified period of time has elapsed from the date of deletion (DeletedAt).
Example :
If you want to remove user and its data after one year, to get users you will remove :
SELECT id FROM users WHERE TO_DAYS(NOW()) >= (TO_DAYS(DeletedAt) + 365) ;
then you will delete data from related tables for these users.
Consider following as an example:
I have a User table which contains user's information along with a PrimaryAddress column that references Address table.
The Address table, contains address information along with a UserId column which refers to who the address belongs to.
Each user can have many addresses, but only one address can be PrimaryAddress. Therefore, the User table needs to store a reference to PrimaryAddress to enforce this rule. Having a IsPrimary column in address table would not have a similar effect.
But as you can tell, this will create a circular relation between User and Address tables and circular dependencies can be a sign of bad design as far as I'm aware. The question is, is there a way to avoid this? If so, how?
A circular reference is not necessarily a "bad" design. You gave an example of a real-world case that has legitimate meaning.
I admit it's a little bit complex to manage. The User.PrimaryAddress must be nullable, if you need to create a user row before you create the address you will eventually designate as the primary address.
Also if you need an SQL script to recreate the database, you can add foreign key constraints only after the referenced table is created. So you have a chicken-and-egg problem if you have circular references. The typical solution is to create tables without their foreign keys, then once they are created, add the foreign keys using ALTER TABLE.
The workarounds all sacrifice something. If you add an attribute Address.IsPrimary, then you have to figure out how to ensure that exactly one address per user is primary. If you use a third table, you have to worry that it is missing a row for each user.
The circular reference may be the least problematic solution.
no you would use a bridge4 table in combination with a BEFORE INSERT TRIGGER
This will also help, if a address can have multiple users
That would then look like
CREATE tABLE user (id int PRIMARY KEY)
INSERT INTO user VALUEs(1)
CREATE TABLe address(id int PRIMARY KEY)
INSERt INTO address VALUES(1),(2)
CREATE TABLE user_address ( user_id int, address_id int, Is_primary int
,FOREIGN KEY (user_id)
REFERENCES user(id)
,FOREIGN KEY (address_id)
REFERENCES address(id)
,PRIMARY KEY (user_id,address_id)
)
CREATE TRIGGER before_user_address_insert
BEFORE INSERT
ON user_address FOR EACH ROW
BEGIN
DECLARE rowcount INT;
IF NEW.Is_primary = 1 then
SELECT COUNT(*)
INTO rowcount
FROM user_address WHERE user_id = NEw.user_id AND Is_primary = 1;
IF rowcount = 1 THEN
set #message_text = CONCAT('userid ',New.user_id ,' has already a primary address');
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = #message_text;
END IF;
end if;
END
INSERT INTO user_address VALUEs(1,1,1)
✓
INSERT INTO user_address VALUEs(1,2,1)
userid 1 has already a primary address
db<>fiddle here
I have a main table called results. E.g.
CREATE TABLE results (
r_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
r_date DATE NOT NULL,
system_id INT NOT NULL,
FOREIGN KEY (system_id) REFERENCES systems(s_id) ON UPDATE CASCADE ON DELETE CASCADE
);
The systems table as:
CREATE TABLE systems (
s_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
system_name VARCHAR(50) NOT NULL UNIQUE
);
I'm writing a program in Python with MySQL connector. Is there a way to add data to the systems table and then auto assign the generated s_id to the results table?
I know I could INSERT into systems, then do another call to that table to see what the ID is for the s_name, to add to the results table but I thought there might be quirk in SQL that I'm not aware of to make life easier with less calls to the DB?
You could do what you describe in a trigger like this:
CREATE TRIGGER t AFTER INSERT ON systems
FOR EACH ROW
INSERT INTO results SET r_date = NOW(), system_id = NEW.s_id;
This is possible only because the columns of your results table are easy to fill in from the data the trigger has access to. The auto-increment fills itself in, and no additional columns need to be filled in. If you had more columns in the results table, this would be harder.
You should read more about triggers:
https://dev.mysql.com/doc/refman/8.0/en/create-trigger.html
https://dev.mysql.com/doc/refman/8.0/en/triggers.html
Following is the relational schema,
college(ID int, name text, grade int);
friend(ID1 int, ID2 int);
likes(ID1 int, ID2 int);
I wrote a trigger that maintains the symmetry in friendship relationships. That means if (X, Y) is deleted from friend, (Y, X) should also be deleted.
Following are the table creations;
create table college
(
id int auto_increment primary key,
name text not null,
grade int not null
);
create table likes
(
id1 int not null,
id2 int
);
create table friend
(
id1 int not null,
id2 int not null,
constraint friend_pk
primary key (id1, id2),
constraint friend_college_id_fk
foreign key (id1) references college (id)
constraint friend_college_id_fk2
foreign key (id2) references college (id)
);
I wrote follwing trigger;
delimiter //
drop trigger if exists Friend_Delete //
create trigger Friend_Delete
after delete
on friend
for each row
begin
delete
from friend
where id1 = OLD.id2
and id2 = OLD.id1;
end //
delimiter ;
But when I execute the following query,
delete from friend where id1 = 1 and id2 = 2;
It gives me this result;
Can't update table 'friend' in stored function/trigger because it is already used by statement which invoked this stored function/trigger
I googled this issue and it is referred as endless recursive calls are taken place. But When we say after delete, it is deleted and why we cannot perform another deletion here?
The database engine is making sure that you cannot write a trigger that sets off a never ending sequence of triggers. It's not looking at the logic of your code, it's just making sure that you don't set off a series of trigger actions that keeps running. It doesn't really matter whether it's "after" the original delete.
In very general terms, I'd recommend handling this in application code, rather than database code. Triggers have a number of problems - they're hard to test, they're hard to debug, they need developers to remember that doing one thing (deleting a row in this case) has a side effect (automagically deleting another row), they can create weird performance issues (a cascading sequence of triggers firing might look like your database is slow).
delete from friend where id1 = 1 and id2 = 2;
With above query you are deleting rows where id1 =1 and id2= 2
Then the trigger is trying to delete all the rows with id1=2 and id2=1
which will also trigger Friend_Delete to delete rows with id1=1 and id2=2 and this is the first delete condition you used.
So this way recursion is happening and an endless loop has been created.
Let's say I have a User table with a username column that has a unique constraint. Now I need to add an alias column that must also be unique, but with the added requirement that no two users can have the same username and alias (user1.username <> user2.alias). How would I go about doing this with MySQL?
I know about composite unique indices, but they check against a combination of username and alias being duplicated, not a combination of one user's username being equal to the new user's alias.
The relational way would be to make another table user_names where each user can have one or many rows in that table.
CREATE TABLE user_name (
user_id INT NOT NULL,
user_name VARCHAR(16) NOT NULL,
PRIMARY KEY (user_id, user_name),
UNIQUE KEY (user_name)
);
The UNIQUE KEY enforces uniqueness across all users.
The composite PRIMARY KEY makes it efficient to join from the users table to the clustered index of user_names.
I would go for a trigger on that table. The specifics you find here --> https://www.siteground.com/kb/mysql-triggers-use/.
Basically you are going to perform your check before every single insert. Not quite a friend of performance, but that would give you what you need.
CREATE TRIGGER aliascheck BEFORE INSERT ON User FOR EACH ROW IF NEW.alias in (select username from User) THEN SIGNAL SQLSTATE '45000' set message_text='Alias already exists!'; END IF;
... Or something like that. I have a TSQL background :)