Problems understanding FOREIGN KEY / CASCADE constraints - sql

I need some help at understanding how foreign keys and cascades work. I understood the theory but I'm having troubles to apply these to a real world example.
Let's assume I've got the following tables (and an arbitrary number of other tables that may reference table tags):
CREATE TABLE tags (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) UNIQUE
) Engine=InnoDB;
CREATE TABLE news (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(63),
content TEXT,
INDEX (title)
) Engine=InnoDB;
So I create a further table to provide the many-to-many relation between news and tags:
CREATE TABLE news_tags (
news_id INT UNSIGNED,
tags_id INT UNSIGNED,
FOREIGN KEY (news_id) REFERENCES news (id) ON DELETE ...,
FOREIGN KEY (tags_id) REFERENCES tags (id) ON DELETE ...
) Engine=InnoDB;
My requirements to the cascades:
If I delete a news, all corresponding entries in news_tags should be removed as well.
Same applies for table x that may be added later with x_tags-table.
If I delete a tag, all corresponding entries in news_tags and in every further table x_tags should be removed as well.
I'm afraid that I may have to revisit my table structure for this purpose, but that's alright since I'm only trying to figure out how stuff works.
Any links to good tutorials, SQL-queries or JPA-examples appreciated!

You seem to be proposing something like this, which sounds reasonable to me:
CREATE TABLE tags
(
id INTEGER NOT NULL,
name VARCHAR(20) NOT NULL,
UNIQUE (id),
UNIQUE (name)
);
CREATE TABLE news
(
id INTEGER NOT NULL,
title VARCHAR(30) NOT NULL,
content VARCHAR(200) NOT NULL,
UNIQUE (id)
);
CREATE TABLE news_tags
(
news_id INTEGER NOT NULL,
tags_id INTEGER NOT NULL,
UNIQUE (tags_id, news_id),
FOREIGN KEY (news_id)
REFERENCES news (id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY (tags_id)
REFERENCES tags (id)
ON DELETE CASCADE
ON UPDATE CASCADE
);

Related

PostgreSQL | How to store user answers in survey?

I have task to create survey which aviable only by invitation to certain people. One person can take the survey once. The survey is only available for a specific period of time. I am tring to create database structure for this future application. As database I use PostgreSQL.
As I understand one survey can have many questions. In the same time one question can be reused in many surveys. It's many to many relationship.
Thats how I created tables which solve this first task. Please correct me if I'm somewhere missed or did wrong.
QUESTIONS TABLE:
CREATE TABLE QUESTIONS(
ID SERIAL PRIMARY KEY,
TEXT TEXT NOT NULL
);
SURVEYS TABLE:
CREATE TABLE SURVEYS(
ID UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
NAME VARCHAR NOT NULL,
DESCRIPTION TEXT,
START_PERIOD TIMESTAMP,
END_PERIOD TIMESTAMP
);
SURVEYS_QUESTIONS TABLE:
CREATE TABLE SURVEYS_QUESTIONS(
ID SERIAL,
SURVEY_ID UUID NOT NULL,
QUESTION_ID INT NOT NULL,
PRIMARY KEY (ID),
FOREIGN KEY (SURVEY_ID) REFERENCES SURVEYS (ID) ON DELETE CASCADE,
FOREIGN KEY (QUESTION_ID) REFERENCES QUESTIONS (ID) ON DELETE CASCADE
);
Right now I don't understand how correctly connect users with surveys and how correctly store user answers.
Create a TAKEN_SURVEYS table with a foreign key to the SURVEYS table and a foreign key to the USERS table. If you want to ensure there can be only one taken survey record per user, create a unique index on the TAKEN_SURVEYS table.
Consider replacing your last table with two new tables. This would align with classic 101 example of Customers-Orders-Products (here being Users-Surveys-Q&A).
CREATE TABLE USER_SURVEYS (
ID SERIAL,
USER_ID UUID NOT NULL,
SURVEY_ID UUID NOT NULL,
PRIMARY KEY (ID),
FOREIGN KEY (USER_ID) REFERENCES USERS (ID) ON DELETE CASCADE,
FOREIGN KEY (SURVEY_ID) REFERENCES SURVEYS (ID) ON DELETE CASCADE
);
CREATE TABLE USER_SURVEYS_QA (
ID SERIAL,
USER_SURVEY_ID INT NOT NULL,
QUESTION_ID INT NOT NULL,
ANSWER VARCHAR(255),
OTHER_SPECIFY VARCHAR(255),
PRIMARY KEY (ID),
FOREIGN KEY (USER_SURVEY_ID) REFERENCES USER_SURVEYS (ID) ON DELETE CASCADE,
FOREIGN KEY (QUESTION_ID) REFERENCES QUESTIONS (ID) ON DELETE CASCADE
);

SQL Server perform a delete on all child records when deleting from a parent

I have 3 tables:
Create TABLE Subjects
(
SubjectID INT PRIMARY KEY NOT NULL IDENTITY(1,1),
SubjectName VARCHAR(20) NOT NULL,
ClassID VARCHAR(10) FOREIGN KEY REFERENCES Classes(ClassID) NOT NULL
);
Create TABLE Topic
(
TopicID INT PRIMARY KEY NOT NULL IDENTITY(1,1),
TopicName VARCHAR(100),
SubjectID INT FOREIGN KEY REFERENCES Subjects(SubjectID)
);
Create Table Worksheet
(
WorksheetName varchar(100) PRIMARY KEY,
TopicID INT Foreign KEY References Topic(TopicID),
Num_Q INT NOT NULL,
W_Type varchar(30)
);
Each one is a one to many relationship. When I try to delete from Subjects I get a foreign key constraint which is fine. What I want to know is how to get around this and perform a query to delete all relating aspects in a cascading style. I looked it up and there's but I am not sure how it works there seems to be multiple queries. Would it be better to create a trigger or is there a basic cascading function to do it all? I'm using visual studio to perform queries but not sure where the options to perform tasks like this are?
You can add the ON DELETE CASCADE right after the foreign key definition:
Create TABLE Subjects (
SubjectID INT PRIMARY KEY NOT NULL IDENTITY(1, 1),
SubjectName VARCHAR(20) NOT NULL,
ClassID VARCHAR(10) NOT NULL
FOREIGN KEY REFERENCES Classes(ClassID) ON DELETE CASCADE
);
You can also define it as a separate constraint, if you like, either within the CREATE TABLE statement or using ALTER TABLE ADD CONSTRAINT.
Here is the DDL for your Topic table with a CASCADE for delete. Its just a matter of defining it in your FK but using a slightly different syntax. This is for MS Sql Server.
CREATE TABLE Topic
(
TopicID INT PRIMARY KEY NOT NULL IDENTITY(1,1),
TopicName VARCHAR(100),
SubjectID INT,
CONSTRAINT FK_Subjects_Topic FOREIGN KEY (SubjectID)
REFERENCES Subjects (SubjectID)
ON DELETE CASCADE
ON UPDATE NO ACTION
)
EDIT - added DELETE CASCADE on Worksheet table based on comment feedback.
Create Table Worksheet
(
WorksheetName varchar(100) PRIMARY KEY,
TopicID INT,
Num_Q INT NOT NULL,
W_Type varchar(30),
CONSTRAINT FK_Topic_Worksheet FOREIGN KEY (TopicID)
REFERENCES Topic (TopicID)
ON DELETE CASCADE
ON UPDATE NO ACTION
);
With this updated definition a delete on table Subjects will also delete child records in table Topics.

How do I create a table whose rows reference 1 (and only 1) of 2 existing tables?

Here's my situation: I have two tables created with
CREATE DATABASE JsPracticeDb;
/* Create tables corresponding to the problems, solutions to
problems, and ratings of problems or solutions */
CREATE TABLE Problems (
id INT PRIMARY KEY NOT NULL,
prompt_code VARCHAR(3000),
test_func_code VARCHAR(3000),
test_input_code VARCHAR(3000)
);
CREATE TABLE Solutions (
id INT PRIMARY KEY NOT NULL,
problem_id INT,
solver_name VARCHAR(50),
code VARCHAR(3000),
FOREIGN KEY (problem_id) REFERENCES Problems(id) ON DELETE CASCADE,
);
and I was thinking about creating a table for rating Solutions, which I wrote as
CREATE TABLE Ratings (
id INT PRIMARY KEY NOT NULL,
solution_id INT,
stars TINYINT,
FOREIGN KEY (solution_id) REFERENCES Solutions(id) ON DELETE CASCADE
);
but then I realized I might actually want to have Problems rated as well. The "brute force" solution, as I see it, is
CREATE TABLE SolutionRatings (
id INT PRIMARY KEY NOT NULL,
solution_id INT,
stars TINYINT,
FOREIGN KEY (solution_id) REFERENCES Solutions(id) ON DELETE CASCADE
);
CREATE TABLE ProblemRatings (
id INT PRIMARY KEY NOT NULL,
problem_id INT,
stars TINYINT,
FOREIGN KEY (problem_id) REFERENCES Problems(id) ON DELETE CASCADE
);
but my programming intuition says there's a problem with the fact that I used copy-paste to write two sections of code that are almost identical. However, I can't think of any alternative solution that uses an intersection table or something like that also allows me to do a cascade delete. For example, I know I could do
CREATE TABLE RatedTables (
id TINYINT PRIMARY KEY NOT NULL,
table_name VARCHAR(9)
);
INSERT INTO RatedTables (table_name) VALUES ('Problems','Solutions');
CREATE TABLE Ratings (
id INT PRIMARY KEY NOT NULL,
rated_table_id TINYINT NOT NULL,
stars TINYINT,
FOREIGN KEY (rated_table_id) REFERENCES RatedTables(id)
);
but then how would I make it so that if a Solution with corresponding Ratings was deleted then those ratings would be too?????
You basically have two options but this is a good opportunity to go back and review your db structure.
The first option is to do something like this:
CREATE TABLE potential_link1 (
id int primary key,
...
);
CREATE TABLE potential_link2 (
id int primary key,
....
);
CREATE TABLE ratings (
id int primary key,
potential_link1 int references potential_link1(id) on delete cascade,
potential_link2 int references potential_link2(id) on delete cascade,
....
check(potential_link1 is null or potential_link2 is null),
check(potential_link2 is not null or potential_link1 is not null)
);
This works but as you can see it is a bit complex.
The second possibility is that since there are clear cases where a is dependent on the union of b and c then you may think about whether you can refactor your db structure to reflect that so you only need one table to link against.
There is nothing wrong with two tables looking so much alike. They contain different things and you won't want to select all three-star ratings no matter whether on problems or solutions for instance - you would always work with solution ratings or problem ratings.
But to have both ratings in one table is also not wrong and can be a good idea when you want ratings to behave the same, no matter whether on problem or solution (e.g. both shall have 1 to 5 stars, both can have a comment no longer then 200 chars, ...).
This could be done by simply giving the ratings table both a problem_id and a solution_id with foreign keys on the tables and fill always one or the other. With natural keys, the same would feel even more, well, natural:
problem(problem_no, data)
solution(problem_no, solution_no, data)
rating(problem_no, solution_no, data)
with rating.solution_no nullable and foreign keys on both parent tables.

Mapping table between two sibling tables impossible in PostgreSQL?

I am building a site with 2 templates: stats and tags but there is also an N:M mapping between them.
Stats and tags both have their own pages on the site and these have common characteristics, so I'd like a parent table called pages.
create table pages (id serial primary key, title varchar(50));
create table stats (show_average boolean) inherits (pages);
create table tags (color varchar(50)) inherits (pages);
create table stat_tags (
stat_id int
,tag_id int
,foreign key (stat_id) references stats(id)
,foreign key (tag_id) references tags(id)
);
This last query yields:
ERROR: there is no unique constraint matching given keys for referenced table "tags"
If I do it without inheritance like this:
create table stats (id serial primary key, title varchar(50), show_average boolean);
create table tags (id serial primary key, title varchar(50), color varchar(50));
create table stat_tags (
stat_id int
,tag_id int
,foreign key (stat_id) references stats(id)
,foreign key (tag_id) references tags(id)
);
... it accepts all queries.
Is it possible for two children to have a mapping table in PostgreSQL? How?
Thanks!
Using inherits probably isn't the best way to do this.
A serious limitation of the inheritance feature is that indexes
(including unique constraints) and foreign key constraints only apply
to single tables, not to their inheritance children. This is true on
both the referencing and referenced sides of a foreign key constraint.
See Caveats
I would be more comfortable with something along these lines.
create table pages (
id serial primary key,
page_type char(1) not null
check (page_type in ('s', 't')),
title varchar(50) not null unique,
unique (id, page_type)
);
create table stats (
id integer primary key,
page_type char(1) not null default 's'
check(page_type = 's'),
show_average boolean,
foreign key (id, page_type) references pages (id, page_type)
);
create table tags (
id integer primary key,
page_type char(1) not null default 't'
check(page_type = 't'),
color varchar(50) not null
);
create table stat_tags (
stat_id int not null,
tag_id int not null,
primary key (stat_id, tag_id),
foreign key (stat_id) references stats(id),
foreign key (tag_id) references tags(id)
);
In production, you'd probably want to build two updatable views, one to resolve the join between pages and stats, and one to resolve the join between pages and tags.

How can I get around this foreign key constraint having to have a unique name?

I'm not sure why these have to be unique, but from reading the MySQL forums it appears that they do. However, I think it has something more to do with the INDEX name. I have two tables that have foreign key constraints referencing the same primary key on a third table. If it helps, I'm using MySQL workbench to design the schema.
I usually name my foreign key on each table the same name as the primary key it references. I guess this isn't possible. It will create the first table with the foreign key constraint, but when it tries to create the second table it throws an error. Here is the second table it throws the error on:
CREATE TABLE IF NOT EXISTS `joe`.`products_to_categories` (
`product_to_category_id` INT NOT NULL AUTO_INCREMENT ,
`category_id` INT NOT NULL ,
`product_id` INT NOT NULL ,
PRIMARY KEY (`product_to_category_id`) ,
INDEX `category_id` (`category_id` ASC) ,
INDEX `product_id` (`product_id` ASC) ,
CONSTRAINT `category_id`
FOREIGN KEY (`category_id` )
REFERENCES `joe`.`categories` (`category_id` )
ON DELETE CASCADE
ON UPDATE NO ACTION,
CONSTRAINT `product_id`
FOREIGN KEY (`product_id` )
REFERENCES `joe`.`products` (`product_id` )
ON DELETE CASCADE
ON UPDATE NO ACTION)
ENGINE = InnoDB;
I want the foreign key names to be the same as the primary key in both of the other tables. What should I remove here so that I can use these names. What is the best practice here.
It is not possible because you would have a conflict in the filename for the file that is used for the index IIRC. I probably would name the key < tablename >_< column_name > or something like that.
You are creating an index (constraint) by the name of product_id via:
INDEX product_id
Then you are going and creating another constraint (for the foreign key) with the same name:
CONSTRAINT product_id
What you need to do is allow the server to provide a default, unique constraint name by removing the
CONSTRAINT product_id
See this URL: http://dev.mysql.com/doc/refman/5.1/en/innodb-foreign-key-constraints.html
"If the CONSTRAINT symbol clause is given, the symbol value must be unique in the database. If the clause is not given, InnoDB creates the name automatically."
In PostgreSQL, the default for naming indexes is to append "_pkey" and "_fkey" to the name of the primary and foreign key, respectively. So your case would look like:
INDEX `product_id_fkey` (`product_id` ASC) ,
UPDATE: I just tried this and it worked. See if that's what you had in mind.
use test;
create table if not exists test.product
(
product_id int not null auto_increment,
name varchar(80) not null,
primary key(product_id)
);
create table if not exists test.category
(
category_id int not null auto_increment,
name varchar(80) not null,
primary key(category_id)
);
create table if not exists test.product_category
(
product_id int,
category_id int,
primary key(product_id, category_id),
constraint product_id_fkey
foreign key(product_id) references product(product_id)
on delete cascade
on update no action,
constraint category_id_fkey
foreign key(category_id) references category(category_id)
on delete cascade
on update no action
);
insert into test.product(name) values('teddy bear');
insert into test.category(name) values('toy');
insert into test.product_category
select p.product_id, c.category_id from product as p, category as c
where p.name = 'teddy bear' and c.name = 'toy';