Check if many-to-many relationship exists before insert or delete - sql

I have 3 tables
For example:
Book
id
title
Tag
id
name
BookTag
book_id
tag_id
The goal to disallow having Book without Tag. i.e. when I try insert/delete data I need something to check on database level that Book has at least one Tag through many-to-many. If such validation fails it should throw constaint violation error or some sort of that. How should I implement that? Can it be reached by check constraint or should I create some trigger, if so then how?
please help me. thanks for your help in advance

You can enforce this at the pure database level by adding a foreign key in the book table that points back to a tag (any tag) in the book_tag table. As of now, your database model looks like:
create table book (
id int primary key not null,
title varchar(50)
);
create table tag (
id int primary key not null,
name varchar(50)
);
create table book_tag (
book_id int not null,
book_tag int not null,
primary key (book_id, book_tag)
);
Now, add the extra foreign key that points back to a tag:
alter table book add column a_tag int not null;
alter table book add constraint fk1 foreign key (id, a_tag)
references book_tag (book_id, tag_id) deferrable initially deferred;
Now when you insert a book, it can temporarily not have a tag, but only while the transaction hasn't finished yet. You need to insert a tag before committing. If you don't the constraint will fail, the transaction will rollback, and the insert won't happen.
Note: Please notice that this requires the use of deferrable constraints (look at deferrable initially deferred), something that is part of the SQL Standard but seldomly implemented. Fortunately, PostgreSQL does.
EDIT - Adding an example
Considering the previous modified tables you can try inserting a book without tags (will fail) and with tags (succeeding) as shown below:
insert into tag (id, name) values (10, 'classic');
insert into tag (id, name) values (12, 'action');
insert into tag (id, name) values (13, 'science fiction');
-- begin transaction
insert into book (id, title, a_tag) values (1, 'Moby Dick', 123);
commit; -- fails
-- begin transaction
insert into book (id, title, a_tag) values (2, 'Frankenstein', 456);
insert into book_tag (book_id, book_tag) values (2, 10);
insert into book_tag (book_id, book_tag) values (2, 13);
update book set a_tag = 10;
commit; -- succeeds

Related

How can I insert values into a table with a composite primary key?

These are the tables I already have:
CREATE TABLE Gyartok
(
GyID INT IDENTITY(2, 3),
Nev VARCHAR(20),
CONSTRAINT PK_Gyartok PRIMARY KEY (GyID)
)
CREATE TABLE Focicsuka
(
CsID INT IDENTITY(2, 2),
Meret INT,
CONSTRAINT PK_Focicsuka PRIMARY KEY (CsID)
)
CREATE TABLE FcsGyartjaGya
(
GyID INT IDENTITY(3, 2),
CsID INT,
Ar INT,
CONSTRAINT FK_FcsGyartjaGya1
FOREIGN KEY (GyID) REFERENCES Gyartok(GyID),
CONSTRAINT FK_FcsGyartjaGya2
FOREIGN KEY (CsID) REFERENCES Focicsuka(CsID),
CONSTRAINT PK_FcsGyartjaGya
PRIMARY KEY (GyID, CsID)
)
The problem is that every time I try to add new values to the table (like such)
INSERT INTO FcsGyartjaGya (Ar) VALUES (300);
I get an error saying I didn't initialize the CsID INT column:
Cannot insert the value NULL into column 'CsID', table 'Lab3.dbo.FcsGyartjaGya'; column does not allow nulls. INSERT fails.
I know I must initialize it with something, but I have no idea what do to it with, because IDENTITY(x, y) doesn't work (it's occupied by another column already) and adding another parameter to the code (like such)
INSERT INTO FcsGyartjaGya (Ar, CsID) VALUES (300, 7);
creates another error which says
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_FcsGyartjaGya1". The conflict occurred in database "Lab3a", table "dbo.Gyartok", column 'GyID'.
It is important to note that I already filled every column with data, so that couldn't be the problem.
As I mention in the comments, your INSERT will work fine, provided the stars align correctly. For your table Gyartok you have GyID as your PRIMARY KEY, which is defined as a IDENTITY(2,3); so the first value generated is 2 and then each row attempted to be INSERTed will increment by 3.
So, if we run the following, we get the IDs 2, 5, 7 and 17. (11 and 14 are skipped as the INSERT failed).
CREATE TABLE Gyartok (
GyID INT IDENTITY(2, 3),
Nev VARCHAR(20),
CONSTRAINT PK_Gyartok PRIMARY KEY (GyID)
);
GO
INSERT INTO dbo.Gyartok (Nev)
VALUES ('asdfjahsbvd'),
('ashjkgdfakd'),
('kldfbhjo');
GO
INSERT INTO dbo.Gyartok (Nev)
VALUES (REPLICATE('A',25)), --Force a truncation error
('ashjkgdfakd');
GO
INSERT INTO dbo.Gyartok (Nev)
VALUES (REPLICATE('A',15));
Let's now add some data for your other table:
CREATE TABLE Focicsuka (
CsID INT IDENTITY(2, 2),
Meret INT,
CONSTRAINT PK_Focicsuka PRIMARY KEY (CsID)
)
INSERT INTO dbo.Focicsuka (Meret)
VALUES(12),
(25);
Now we want to INSERT into the table FcsGyartjaGya, defined as the following:
CREATE TABLE FcsGyartjaGya (
GyID INT IDENTITY(3, 2),
CsID INT,
Ar INT,
CONSTRAINT FK_FcsGyartjaGya1 FOREIGN KEY (GyID) REFERENCES Gyartok(GyID),
CONSTRAINT FK_FcsGyartjaGya2 FOREIGN KEY (CsID) REFERENCES Focicsuka(CsID),
CONSTRAINT PK_FcsGyartjaGya PRIMARY KEY (GyID, CsID)
)
This has a IDENTITY on GyID, but defined as an IDENTITY(3,2), so the first value is 3 and then incremented by 2.
As this has 2 foreign keys, on GyID and CsID when we INSERT the row the values must appear in the respective tables. As GyID is defined as anIDENTITY(3,2) however, this is where we need to rely on the Stars luck for the INSERT to work. Why? Well 2 + (3*n) and 3+(2*n) can give very different numbers. The first are as you saw at the start of this answer. For the latter, we have numbers like 3, 5, 7, 9, 11. As you can see, only 1 in 3 of these numbers match a number in our original sequence, so luck is what we are going to be relying on.
Let's, therefore, try a single INSERT.
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(2,1);
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_FcsGyartjaGya1". The conflict occurred in database "Sandbox", table "dbo.Gyartok", column 'GyID'.
Well, that didn't work, but it was expected. 3 isn't a value in the table Gyartok. Let's try again!
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(2,2);
It worked! The stars Luck was our side, and the IDENTITY value was a value in the table Gyartok. Let's try a couple of rows this time!
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,3),
(4,4);
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_FcsGyartjaGya1". The conflict occurred in database "Sandbox", table "dbo.Gyartok", column 'GyID'.
No!! Not again. :( That's because the stars didn't align; 7 and 9 aren't in the other table. But wait, 11 was in the sequence, so let's try that:
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,5);
Error, again?! No, it cannot be!!! :( Oh wait, I forgot, the stars were against us before, because that INSERT failed against Gyartok for the value of 11. I need to wait for 17!
--13 fails
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,6);
GO
--15 fails
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,6);
GO
--17 works!
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,6);
And now we have another row in our table.
So what is the problem? Your design. GyID is defined as an IDENTITY and a FOREIGN KEY; meaning you are at the "whims" of SQL Server generating a value valid. This is not what you want. Just don't define the column as an IDENTITY and then INSERT the data with all 3 of your columns defined:
CREATE TABLE FcsGyartjaGya (
GyID int,-- IDENTITY(3, 2),
CsID INT,
Ar INT,
CONSTRAINT FK_FcsGyartjaGya1 FOREIGN KEY (GyID) REFERENCES Gyartok(GyID),
CONSTRAINT FK_FcsGyartjaGya2 FOREIGN KEY (CsID) REFERENCES Focicsuka(CsID),
CONSTRAINT PK_FcsGyartjaGya PRIMARY KEY (GyID, CsID)
)
GO
INSERT INTO dbo.FcsGyartjaGya (GyID, CsID, Ar)
VALUES(2,2,1),
(2,4,2),
(5,4,3),
(8,2,4),
(8,4,5);
And all these rows insert fine.
I think there is a bit confusion, if I understand correctly what You're trying to do, then you have two tables each with their own id, which is based on an identity column, so you get new values in those for free.
Then you are trying to make a relation table with extra data.
Issue 1: You cannot have FcsGyartjaGya.GyID be identity if it refers to Gyartok.GyID because you will want to insert into it and not rely on an auto increment. If it doesn't refer to the same it should have another name or my head will possibly explode :))
Issue 2: When populating a relation table you need to insert it with what pairs you want, there is no way SQL server can know how it should match these identity pairs in the relation table
I think this is what people are aiming at in the comments, for example
to insert a relationship between row with Focicsuka.CsID = 1 to Gyartok.GyID 7 and adding Ar = 300 have to look like
INSERT INTO FCSGYARTJAGYA(GYID, CSID, AR)
VALUES(7, 1, 300)
Unless You've forgotten to mention that you want to put some value for each of some value or based on something which can be scripted, in other words unless You have logics to define the pairs and their values, relationship tables cannot have defaults on their foreign key fields.

Violating foreign key constraint with deferred constraint

I'm trying to set my sql scripts into a transaction to achieve atomicity with my database.
The table structure is (simplified):
CREATE TABLE foo (
id serial NOT NULL,
foo varchar(50) NOT NULL,
CONSTRAINT foo_pk PRIMARY KEY (id)
);
CREATE TABLE access (
id serial NOT NULL,
foo_id int NULL
CONSTRAINT access_pk PRIMARY KEY (id)
);
ALTER TABLE access ADD CONSTRAINT access_foo
FOREIGN KEY (foo_id)
REFERENCES foo (id)
ON DELETE CASCADE
ON UPDATE CASCADE
DEFERRABLE
INITIALLY DEFERRED;
In my code I first declare:
client.query('BEGIN'); (I'm using npm library 'pg') then
insert a row into a table 'foo', then another insert to 'access' with a foo_id from the first insert. After that there is client.query('COMMIT');
All of this is in a try catch, and in the catch is client.query('ROLLBACK'); and the rolling back seems to be working if there is an issue either of the inserts. When everything should be committed I still end up in the catch block for this:
message: "insert or update on table "access" violates foreign key constraint "access_foo""
detail: "Key (foo_id)=(20) is not present in table "foo"."
I thought that deferring constraint would be enough to do this, but I guess I'm wrong. Any help is welcome.
You probably have some issue with the transaction demarcation. I ran a simple test and works wells.
insert into foo (id, foo) values (1, 'Anne');
start transaction;
insert into access (id, foo_id) values (101, 1);
insert into access (id, foo_id) values (107, 7); -- 7 does not exist yet...
insert into foo (id, foo) values (7, 'Ivan'); -- 7 now exists!
commit; -- at this point all is good
See running example at DB Fiddle.

A simplified version of Twitter. Understanding many-to-many relationships between tables in the database

I am reading this article about a Twitter-like application. The type of storage where tweets, users, likes, etc. will be stored is a relational database. The database scheme is described here and drawn here.
As an Android developer, I coded my sample using SQLite. This is how I would code it:
create table users (_id integer primary key, username text unique, first_name text, last_name text);
create table tweets (_id integer primary key, content text, created_at integer, user_id integer, foreign key(user_id) references users(_id));
create table connections (_id integer primary key, follower_id integer, followee_id integer, created_at integer, foreign key(follower_id) references users(_id), foreign key (followee_id) references users(_id));
create table favorites (_id integer primary key, user_id integer, tweet_id integer, foreign key (user_id) references users(_id), foreign key (tweet_id) references tweets(_id));
Now let's insert some data.
users:
insert into users values (1, 'user1', 'Lorem', 'Ipsum');
insert into users values (2, 'user2', 'Dolor', 'Sit');
insert into users values (3, 'user3', 'Foo', 'Bar');
insert into users values (4, 'user4', 'Qwerty', 'Trewq');
some tweets:
insert into tweets values(10, '1 Tweet from user1', 1100, 1);
insert into tweets values(11, '2 Tweet from user1', 1101, 1);
insert into tweets values(12, '3 Tweet from user1', 1102, 1);
insert into tweets values(13, '4 Tweet from user1', 1103, 1);
insert into tweets values(14, '1 Tweet from user2', 1103, 2);
insert into tweets values(15, '2 Tweet from user2', 1103, 2);
insert into tweets values(16, '1 Tweet from user3', 1103, 3);
insert into tweets values(17, '2 Tweet from user3', 1103, 3);
insert into tweets values(18, '1 Tweet from user4', 1107, 4);
favorites (the same as likes):
insert into favorites values(1, 2, 11);
insert into favorites values(2, 3, 13);
insert into favorites values(3, 4, 15);
There is a question about the database scheme:
Do you think you could support with our database design the ability
to display a page for a given user with their latest tweets that were
favorited at least once?
Yes, this is why query:
sqlite> select favorites._id, tweets._id as tweet_row_id, tweets.content from favorites join tweets on tweets.user_id=1 and tweets._id = favorites.tweet_id order by tweets._id desc limit 1;
_id tweet_row_id content
---------- ------------ ------------------
2 13 4 Tweet from user1
Explanation:
The left dataset is the table favorites. The right dataset is the table tweets. I join the two datasets. Then tweets.user_id=1 and tweets._id = favorites.tweet_id is evaluated for each row of the resulting dataset as a boolean expression. If the result is true, the row is included. order by tweets._id desc is used to get the latest tweets (the greater tweets._id is, the newer the tweet is). limit is used to limit the number of rows. If the user has been using our Twitter-like app for years, we'll show the latest 10 or 20 tweets.
My questions.
Is there anything wrong with my database scheme? I omitted not null, unique, and other column constraints for simplicity.
Here the author of the original article says:
The first relation is addressed by sticking the user ID to each tweet.
This is possible because each tweet is created by exactly one user.
It’s a bit more complicated when it comes to following users and
favoriting tweets. The relationship there is many-to-many.
"The first relation" is users-tweets.
Why do we need a many-to-many here? In my scheme I only use a one-to-many.
Update 1
Shortly I placed an answer here, where the OP - like you in this question - was unsure about 1:n and n:m.
I assume, that your final sentence is the actual question you have:
Why do we need a many-to-many here? In my scheme I only use a one-to-many
The relation user-tweets is 1:n...
Think in objects
user (id, name, ...)
tweet (id, author (FK on user), datetime, content, ...)
The like is an object with sepecific details on its own:
like (id, userid,tweetid,datetime,...)
For this you need a mapping table (you call it favourites)
There is a 1:n-relation from users to this mapping and a 1:n-relation from tweets to this mapping.
These two 1:n-relations form the m:n-relation together.
Now each tweet can be liked by many users and each user can like many tweets, but one user should (probably) not like the same tweet twice (unique key or even a two column PK?). And you might introduce a CHECK constraint to ensure, that the liking user and the author's userid is not the same (don't like your own tweets).
As a side note:
Is there anything wrong with my database scheme
You should never create constraints wihtout naming them
CREATE TABLE Dummy
(
ID INT IDENTITY CONSTRAINT PK_Dummy PRIMARY KEY
,UserID INT NOT NULL CONSTRAINT FK_Dummy_UserID FOREIGN KEY REFERENCES User(id)
,...
)
If this database was ever installed on different systems, they'll get different (random) names and future upgrade scripts will get you in deepest pain...
UPDATE: example for the side note
In you comment you ask, what this last sentence is about... Try this
CREATE DATABASE testDB;
GO
USE testDB;
GO
CREATE TABLE testTbl1(ID INT IDENTITY PRIMARY KEY,SomeValue INT UNIQUE);
CREATE TABLE testTbl2(ID INT IDENTITY PRIMARY KEY,FKtoTbl1 INT NOT NULL FOREIGN KEY REFERENCES testTbl1(ID));
GO
CREATE TABLE testTbl3(ID INT IDENTITY CONSTRAINT PK_3 PRIMARY KEY,SomeValue INT CONSTRAINT UQ_3_SomeValue UNIQUE);
CREATE TABLE testTbl4(ID INT IDENTITY CONSTRAINT PK_4 PRIMARY KEY,FKtoTbl3 INT NOT NULL CONSTRAINT FK_4_FKtoTbl3 FOREIGN KEY REFERENCES testTbl3(ID));
GO
SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS;
GO
USE master;
GO
DROP DATABASE testDB;
GO
On column in your result looks like this:
CONSTRAINT_NAME
------------------------------
PK__testTbl1__3214EC27ABEA2C0C
UQ__testTbl1__0E5C381C04C8AF66
PK__testTbl2__3214EC272784631C
FK__testTbl2__FKtoTb__1367E606
PK_3
UQ_3_SomeValue
PK_4
FK_4_FKtoTbl3
If this script is run twice, the given names will stay as you defined them. The other names will get a random name like PK__testTbl1__3214EC27ABEA2C0C. Now imagine, you need to create an upgrade script for several installed systems where one constraint has to be dropped or modified. How would you do this, if you do not know its name?

sql trigger not firing with insert

DROP TABLE ENROLLMENT CASCADE CONSTRAINTS;
DROP TABLE SECTION CASCADE CONSTRAINTS;
CREATE TABLE SECTION
(
SectionID CHAR(5),
Course VARCHAR2(7),
Students NUMBER(2,0) DEFAULT 0,
CONSTRAINT PK_SECTION
PRIMARY KEY (SectionID)
);
CREATE TABLE ENROLLMENT
(
SectionID CHAR(5),
StudentID CHAR(7),
CONSTRAINT PK_ENROLLMENT
PRIMARY KEY (SectionID, StudentID),
CONSTRAINT FK_ENROLLMENT_SECTION
FOREIGN KEY (SectionID)
REFERENCES SECTION (SectionID)
);
INSERT INTO SECTION (SectionID, Course) VALUES ( '12345', 'CSC 355' );
INSERT INTO SECTION (SectionID, Course) VALUES ( '22109', 'CSC 309' );
INSERT INTO SECTION (SectionID, Course) VALUES ( '99113', 'CSC 300' );
INSERT INTO SECTION (SectionID, Course) VALUES ( '99114', 'CSC 300' );
SELECT * FROM SECTION;
COMMIT;
CREATE TRIGGER AddStudent AFTER INSERT ON ENROLLMENT
BEGIN
DBMS_OUTPUT.PUT_LINE('DONE');
END;
All I am trying to see is if the trigger is fired and when I run a script like INSERT INTO enrollment VALUES('12345','1234567');
I have no output, just "1 row inserted" but I do not get "done" making me thing the trigger is not fired.
The only observable effect of your insert trigger is this:
DBMS_OUTPUT.PUT_LINE('DONE');
This just puts a message into the debug buffer. To see the result, you need to enable DBMS Output.
You can do this in SQL Developer via the View menu, choose Dbms Output, and click the green "+" button. Then run your code and you should see the output, if any.
If you use SQL*Plus instead, you would run SET SERVEROUT ON.

how to create a Foreign-Key constraint to a subset of the rows of a table?

I have a reference table, say OrderType that collects different types of orders:
CREATE TABLE IF NOT EXISTS OrderType (name VARCHAR);
ALTER TABLE OrderType ADD PRIMARY KEY (name);
INSERT INTO OrderType(name) VALUES('sale-order-type-1');
INSERT INTO OrderType(name) VALUES('sale-order-type-2');
INSERT INTO OrderType(name) VALUES('buy-order-type-1');
INSERT INTO OrderType(name) VALUES('buy-order-type-2');
I wish to create a FK constraint from another table, say SaleInformation, pointing to that table (OrderType). However, I am trying to express that not all rows of OrderType are eligible for the purposes of that FK (it should only be sale-related order types).
I thought about creating a view of table OrderType with just the right kind of rows (view SaleOrderType) and adding a FK constraint to that view, but PostgreSQL balks at that with:
ERROR: referenced relation "SaleOrderType" is not a table
So it seems I am unable to create a FK constraint to a view (why?). Am I only left with the option of creating a redundant table to hold the sale-related order types? The alternative would be to simply allow the FK to point to the original table, but then I am not really expressing the constraint as strictly as I would like to.
I think your schema should be something like this
create table order_nature (
nature_id int primary key,
description text
);
insert into order_nature (nature_id, description)
values (1, 'sale'), (2, 'buy')
;
create table order_type (
type_id int primary key,
description text
);
insert into order_type (type_id, description)
values (1, 'type 1'), (2, 'type 2')
;
create table order_nature_type (
nature_id int references order_nature (nature_id),
type_id int references order_type (type_id),
primary key (nature_id, type_id)
);
insert into order_nature_type (nature_id, type_id)
values (1, 1), (1, 2), (2, 1), (2, 2)
;
create table sale_information (
nature_id int default 1 check (nature_id = 1),
type_id int,
foreign key (nature_id, type_id) references order_nature_type (nature_id, type_id)
);
If the foreign key clause would also accept an expression the sale information could omit the nature_id column
create table sale_information (
type_id int,
foreign key (1, type_id) references order_nature_type (nature_id, type_id)
);
Notice the 1 in the foreign key
You could use an FK to OrderType to ensure referential integrity and a separate CHECK constraint to limit the order types.
If your OrderType values really are that structured then a simple CHECK like this would suffice:
check (c ~ '^sale-order-type-')
where c is order type column in SaleInformation
If the types aren't structured that way in reality, then you could add some sort of type flag to OrderType (say a boolean is_sales column), write a function which uses that flag to determine if an order type is a sales order:
create or replace function is_sales_order_type(text ot) returns boolean as $$
select exists (select 1 from OrderType where name = ot and is_sales);
$$ language sql
and then use that in your CHECK:
check(is_sales_order_type(c))
You don't of course have to use a boolean is_sales flag, you could have more structure than that, is_sales is just for illustrative purposes.