I have table tariffs, with two columns: (tariff_id, reception)
I have table users, with two columns: (user_id, reception)
And I have table users_tariffs with two columns: (user_id, tariff_id).
I want to prevent situation when tariff from one reception is assigned to user from another reception. How can I do that?
E.G
Users:
user_id | reception
Putin | Russia
Trump | USA
Tariffs:
tariff_id | reception
cheap | USA
expensive | Russia
Wrong situation at users_tariffs, because Cheap tariff is for USA only:
user_id | tariff_id
Putin | Cheap
SOLUTION 1: FOREIGN KEY CONSTRAINTS
I am assuming the following table definitions.
In particular, the composite key in user_tariffs makes this a many-to-many relationship between users and tariffs.
CREATE TABLE tariffs (tariff_id int NOT NULL PRIMARY KEY,
reception text NOT NULL);
CREATE TABLE users (user_id int NOT NULL PRIMARY KEY,
reception text NOT NULL);
CREATE TABLE user_tariffs (tariff_id int NOT NULL REFERENCES tariffs (tariff_id),
user_id int NOT NULL REFERENCES users (user_id),
PRIMARY KEY (tariff_id, user_id));
You probably need a combination of all three columns somewhere, so let's create this:
ALTER TABLE user_tariffs ADD COLUMN reception text;
UPDATE user_tariffs a
SET reception = b.reception
FROM (SELECT * FROM tariffs) b
WHERE a.tariff_id = b.tariff_id;
ALTER TABLE user_tariffs ALTER COLUMN reception SET NOT NULL;
Now we can use FOREIGN KEY REFERENCES (user_id, reception) into users.
CREATE UNIQUE INDEX ON tariffs (tariff_id, reception);
ALTER TABLE user_tariffs ADD FOREIGN KEY (tariff_id, reception)
REFERENCES tariffs (tariff_id, reception);
In addition, we can use FK REFs (tariff_id, reception) into tariffs.
CREATE UNIQUE INDEX ON users (user_id, reception);
ALTER TABLE user_tariffs ADD FOREIGN KEY (user_id, reception)
REFERENCES users (user_id, reception);
Populate with data:
INSERT INTO users VALUES (1, 'cheap'), (2, 'expensive');
INSERT INTO tariffs VALUES (1, 'cheap'), (2, 'expensive');
Now assume we have the following data (user_id, tariff_id) to insert:
WITH data (user_id, tariff_id)
AS (VALUES (1, 2), (2, 1)), -- here is your application data
datas (user_id, tariff_id, reception)
AS (SELECT user_id,
tariff_id,
(SELECT u.reception -- reception calculated by user
FROM users u
WHERE u.user_id = d.user_id)
FROM data d)
INSERT INTO user_tariffs SELECT * FROM datas ;
Then you cannot insert the data, because you can only add (1, 1) or (2, 2) with the same reception, but not (1, 2) or (2, 1) with different reception's. The error message is:
ERROR: insert or update on table "user_tariffs" violates foreign key constraint "user_tariffs_user_id_fkey1"
DETAIL: Key (user_id, reception)=(2, cheap) is not present in table "users".
But you can insert with data AS VALUES (1, 1), (2, 2).
I think the FOREIGN KEY CONSTRAINT solution is to be preferred.
Please describe your functional dependencies, if you want better table designs.
SOLUTION 2: TRIGGER
-- DROP TABLE user_tariffs CASCADE;
-- DROP TABLE users CASCADE;
-- DROP TABLE tariffs CASCADE;
CREATE TABLE tariffs (tariff_id int NOT NULL PRIMARY KEY,
reception text NOT NULL);
CREATE TABLE users (user_id int NOT NULL PRIMARY KEY,
reception text NOT NULL);
CREATE TABLE user_tariffs (tariff_id int NOT NULL REFERENCES tariffs (tariff_id),
user_id int NOT NULL REFERENCES users (user_id),
PRIMARY KEY (tariff_id, user_id));
INSERT INTO users VALUES (1, 'cheap'), (2, 'expensive');
INSERT INTO tariffs VALUES (1, 'cheap'), (2, 'expensive');
-- table user_tariffs (user_id, tariff_id) only, without reception column.
Create a function with return type trigger:
CREATE OR REPLACE FUNCTION check_reception()
RETURNS trigger AS $$
DECLARE valid boolean := false;
BEGIN
SELECT (SELECT u.reception FROM users u WHERE u.user_id = NEW.user_id)
= (SELECT t.reception FROM tariffs t WHERE t.tariff_id = NEW.tariff_id)
INTO valid FROM user_tariffs ;
IF valid = false
THEN RAISE EXCEPTION '(user, tariff, reception) invalid.';
END IF;
RETURN NEW;
END; $$ LANGUAGE plpgsql ;
and register it:
CREATE TRIGGER reception_trigger
AFTER INSERT OR UPDATE ON user_tariffs
FOR EACH ROW EXECUTE PROCEDURE check_reception();
Now try to insert (1, 2), which would be (cheap, expensive) and is not allowed:
INSERT INTO user_tariffs VALUES (1, 2);
ERROR: (user, tariff, reception) invalid.
KONTEXT: PL/pgSQL function check_reception() line 7 at RAISE
But we can insert (1, 1), which is (cheap, cheap) without problem:
INSERT INTO user_tariffs VALUES (1, 1);
SELECT * FROM user_tariffs;
Remark
Triggers are not the best solution here, in my opinion. Try to avoid triggers, if possible. They can have side effects (transactions etc). Check StackOverflow for further details :)
Related
I have to create an api with a database that I will implement later using node.js
I have this one problem though :
I want my foreign key to hold multiple id's because a product can have multiple region. Is there any way I can do it ? I am using postgreSQL
CREATE TABLE product(
id SERIAL PRIMARY KEY,
productName VARCHAR(128),
productCode VARCHAR(10)
);
CREATE TABLE region(
id SERIAL PRIMARY KEY,
regionName VARCHAR(128)
);
ALTER TABLE product ADD COLUMN idRegion INT;
ALTER TABLE product ADD CONSTRAINT idRegion FOREIGN KEY (idRegion) REFERENCES region(id);
INSERT INTO region (id, regionname) VALUES
(1, 'US'),
(2, 'EUR'),
(3, 'AEJ'),
(4, 'JPY'),
(5, 'EM'),
(6, 'CAD'),
(7, 'Brazil and LatAM');
INSERT INTO product (productName, productcode, idRegion) VALUES
('Index Option', 'IO', (1,2,3,4,5,6,7));
I created a third table thank you guys for your answers
CREATE TABLE productRegion(
idRegion INT,
idProduct INT
);
ALTER TABLE productRegion ADD CONSTRAINT idRegion FOREIGN KEY (idRegion) REFERENCES region(id);
ALTER TABLE productRegion ADD CONSTRAINT idProduct FOREIGN KEY (idProduct) REFERENCES product(id);
ALTER TABLE productRegion ADD PRIMARY KEY (idRegion, idProduct);
I want to create e temp table using select into syntax. Like:
select top 0 * into #AffectedRecord from MyTable
Mytable has a primary key. When I insert record using merge into syntax primary key be a problem. How could I drop pk constraint from temp table
The "SELECT TOP (0) INTO.." trick is clever but my recommendation is to script out the table yourself for reasons just like this. SELECT INTO when you're actually bringing in data, on the other hand, is often faster than creating the table and doing the insert. Especially on 2014+ systems.
The existence of a primary key has nothing to do with your problem. Key Constraints and indexes don't get created when using SELECT INTO from another table, the data type and NULLability does. Consider the following code and note my comments:
USE tempdb -- a good place for testing on non-prod servers.
GO
IF OBJECT_ID('dbo.t1') IS NOT NULL DROP TABLE dbo.t1;
IF OBJECT_ID('dbo.t2') IS NOT NULL DROP TABLE dbo.t2;
GO
CREATE TABLE dbo.t1
(
id int identity primary key clustered,
col1 varchar(10) NOT NULL,
col2 int NULL
);
GO
INSERT dbo.t1(col1) VALUES ('a'),('b');
SELECT TOP (0)
id, -- this create the column including the identity but NOT the primary key
CAST(id AS int) AS id2, -- this will create the column but it will be nullable. No identity
ISNULL(CAST(id AS int),0) AS id3, -- this this create the column and make it nullable. No identity.
col1,
col2
INTO dbo.t2
FROM t1;
Here's the (cleaned up for brevity) DDL for the new table I created:
-- New table
CREATE TABLE dbo.t2
(
id int IDENTITY(1,1) NOT NULL,
id2 int NULL,
id3 int NOT NULL,
col1 varchar(10) NOT NULL,
col2 int NULL
);
Notice that the primary key is gone. When I brought in id as-is it kept the identity. Casting the id column as an int (even though it already is an int) is how I got rid of the identity insert. Adding an ISNULL is how to make a column nullable.
By default, identity insert is set to off here to this query will fail:
INSERT dbo.t2 (id, id3, col1) VALUES (1, 1, 'x');
Msg 544, Level 16, State 1, Line 39
Cannot insert explicit value for identity column in table 't2' when IDENTITY_INSERT is set to OFF.
Setting identity insert on will fix the problem:
SET IDENTITY_INSERT dbo.t2 ON;
INSERT dbo.t2 (id, id3, col1) VALUES (1, 1, 'x');
But now you MUST provide a value for that column. Note the error here:
INSERT dbo.t2 (id3, col1) VALUES (1, 'x');
Msg 545, Level 16, State 1, Line 51
Explicit value must be specified for identity column in table 't2' either when IDENTITY_INSERT is set to ON
Hopefully this helps.
On a side-note: this is a good way to play around with and understand how select insert works. I used a perm table because it's easier to find.
I have the following three tables:
Please note that the below DDL came models generated by Django then grabbed out of Postgresql after they were created. So modifying the tables is not an option.
CREATE TABLE "parentTeacherCon_grade"
(
id INTEGER PRIMARY KEY NOT NULL,
"currentGrade" VARCHAR(2) NOT NULL
);
CREATE TABLE "parentTeacherCon_parent"
(
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(50) NOT NULL,
grade_id INTEGER NOT NULL
);
CREATE TABLE "parentTeacherCon_teacher"
(
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(50) NOT NULL
);
CREATE TABLE "parentTeacherCon_teacher_grade"
(
id INTEGER PRIMARY KEY NOT NULL,
teacher_id INTEGER NOT NULL,
grade_id INTEGER NOT NULL
);
ALTER TABLE "parentTeacherCon_parent" ADD FOREIGN KEY (grade_id) REFERENCES "parentTeacherCon_grade" (id);
CREATE INDEX "parentTeacherCon_parent_5c853be8" ON "parentTeacherCon_parent" (grade_id);
CREATE INDEX "parentTeacherCon_teacher_5c853be8" ON "parentTeacherCon_teacher" (grade_id);
ALTER TABLE "parentTeacherCon_teacher_grade" ADD FOREIGN KEY (teacher_id) REFERENCES "parentTeacherCon_teacher" (id);
ALTER TABLE "parentTeacherCon_teacher_grade" ADD FOREIGN KEY (grade_id) REFERENCES "parentTeacherCon_grade" (id);
CREATE UNIQUE INDEX "parentTeacherCon_teacher_grade_teacher_id_20e07c38_uniq" ON "parentTeacherCon_teacher_grade" (teacher_id, grade_id);
CREATE INDEX "parentTeacherCon_teacher_grade_d9614d40" ON "parentTeacherCon_teacher_grade" (teacher_id);
CREATE INDEX "parentTeacherCon_teacher_grade_5c853be8" ON "parentTeacherCon_teacher_grade" (grade_id);
My Question is: How do I write an insert statement (or statements) where I do not have keep track of the IDs? More specifically I have a teacher table, where teachers can teach relate to more than one grade and I am attempting to write my insert statements to start populating my DB. Such that I am only declaring a teacher's name, and grades they relate to.
For example, if I have a teacher that belong to only one grade then the insert statement looks like this.
INSERT INTO "parentTeacherCon_teacher" (name, grade_id) VALUES ('foo bar', 1 );
Where grades K-12 are enumerated 0,12
But Need to do something like (I realize this does not work)
INSERT INTO "parentTeacherCon_teacher" (name, grade_id) VALUES ('foo bar', (0,1,3) );
To indicate that this teacher relates to K, 1, and 3 grades
leaving me with this table for the parentTeacherCon_teacher_grade
+----+------------+----------+
| id | teacher_id | grade_id |
+----+------------+----------+
| 1 | 3 | 0 |
| 2 | 3 | 1 |
| 3 | 3 | 3 |
+----+------------+----------+
This is how I can currently (successfully) insert into the Teacher Table.
INSERT INTO public."parentTeacherCon_teacher" (id, name) VALUES (3, 'Foo Bar');
Then into the grade table
INSERT INTO public.parentTeacherCon_teacher_grade (id, teacher_id, grade_id) VALUES (1, 3, 0);
INSERT INTO public.parentTeacherCon_teacher_grade (id, teacher_id, grade_id) VALUES (2, 3, 1);
INSERT INTO public.parentTeacherCon_teacher_grade (id, teacher_id, grade_id) VALUES (3, 3, 3);
A bit more information.
Here is a diagram of the database
Other things I have tried.
WITH i1 AS (INSERT INTO "parentTeacherCon_teacher" (name) VALUES ('foo bar')
RETURNING id) INSERT INTO "parentTeacherCon_teacher_grade"
SELECT
i1.id
, v.val
FROM i1, (VALUES (1), (2), (3)) v(val);
Then I get this error.
[2016-08-10 16:07:46] [23502] ERROR: null value in column "grade_id" violates not-null constraint
Detail: Failing row contains (6, 1, null).
If you want to insert all three rows in one statement, you can use:
INSERT INTO "parentTeacherCon_teacher" (name, grade_id)
SELECT 'foo bar', g.grade_id
FROM (SELECT 0 as grade_id UNION ALL SELECT 1 UNION ALL SELECT 3) g;
Or, if you prefer:
INSERT INTO "parentTeacherCon_teacher" (name, grade_id)
SELECT 'foo bar', g.grade_id
FROM (VALUES (0), (2), (3)) g(grade_id);
EDIT:
In Postgres, you can have data modification statements as a CTE:
WITH i as (
INSERT INTO public."parentTeacherCon_teacher" (id, name)
VALUES (3, 'Foo Bar')
RETURNING *
)
INSERT INTO "parentTeacherCon_teacher" (name, teacher_id, grade_id)
SELECT 'foo bar', i.id, g.grade_id
FROM (VALUES (0), (2), (3)) g(grade_id) CROSS JOIN
i
I have the following scenario in a Postgres 9.3 database:
Tables B and C reference Table A.
Table C has an optional field that references table B.
I would like to ensure that for each row of table C that references table B, c.b.a = c.a. That is, if C has a reference to B, both rows should point at the same row in table A.
I could refactor table C so that if c.b is specified, c.a is null but that would make queries joining tables A and C awkward.
I might also be able to make table B's primary key include its reference to table A and then make table C's foreign key to table B include table C's reference to table A but I think this adjustment would be too awkward to justify the benefit.
I think this can be done with a trigger that runs before insert/update on table C and rejects operations that violate the specified constraint.
Is there a better way to enforce data integrity in this situation?
There is a very simple, bullet-proof solution. Works for Postgres 9.3 - when the original question was asked. Works for the current Postgres 13 - when the question in the bounty was added:
Would like information on if this is possible to achieve without database triggers
FOREIGN KEY constraints can span multiple columns. Just include the ID of table A in the FK constraint from table C to table B. This enforces that linked rows in B and C always point to the same row in A. Like:
CREATE TABLE a (
a_id int PRIMARY KEY
);
CREATE TABLE b (
b_id int PRIMARY KEY
, a_id int NOT NULL REFERENCES a
, UNIQUE (a_id, b_id) -- redundant, but required for FK
);
CREATE TABLE c (
c_id int PRIMARY KEY
, a_id int NOT NULL REFERENCES a
, b_id int
, CONSTRAINT fk_simple_and_safe_solution
FOREIGN KEY (a_id, b_id) REFERENCES b(a_id, b_id) -- THIS !
);
Minimal sample data:
INSERT INTO a(a_id) VALUES
(1)
, (2);
INSERT INTO b(b_id, a_id) VALUES
(1, 1)
, (2, 2);
INSERT INTO c(c_id, a_id, b_id) VALUES
(1, 1, NULL) -- allowed
, (2, 2, 2); -- allowed
Disallowed as requested:
INSERT INTO c(c_id, a_id, b_id) VALUES (3,2,1);
ERROR: insert or update on table "c" violates foreign key constraint "fk_simple_and_safe_solution"
DETAIL: Key (a_id, b_id)=(2, 1) is not present in table "b".
db<>fiddle here
The default MATCH SIMPLE behavior of FK constraints works like this (quoting the manual):
MATCH SIMPLE allows any of the foreign key columns to be null; if any of them are null, the row is not required to have a match in the referenced table.
So NULL values in c(b_id) are still allowed (as requested: "optional field"). The FK constraint is "disabled" for this special case.
We need the logically redundant UNIQUE constraint on b(a_id, b_id) to allow the FK reference to it. But by making it out to be on (a_id, b_id) instead of (b_id, a_id), it is also useful in its own right, providing a useful index on b(a_id) to support the other FK constraint, among other things. See:
Is a composite index also good for queries on the first field?
(An additional index on c(a_id) is typically useful accordingly.)
Further reading:
Differences between MATCH FULL, MATCH SIMPLE, and MATCH PARTIAL?
Enforcing constraints “two tables away”
I ended up creating a trigger as follows:
create function "check C.A = C.B.A"()
returns trigger
as $$
begin
if NEW.b is not null then
if NEW.a != (select a from B where id = NEW.b) then
raise exception 'a != b.a';
end if;
end if;
return NEW;
end;
$$
language plpgsql;
create trigger "ensure C.A = C.B.A"
before insert or update on C
for each row
execute procedure "check C.A = C.B.A"();
Would like information on if this is possible to achieve without database triggers
Yes, it is possible. The mechanism is called ASSERTION and it is defined in SQL-92 Standard(though it is not implemented by any major RDBMS).
In short it allows to create multiple-row constraints or multi-table check constraints.
As for PostgreSQL it could be emulated by using view with WITH CHECK OPTION and performing operation on view instead of base table.
WITH CHECK OPTION
This option controls the behavior of automatically updatable views. When this option is specified, INSERT and UPDATE commands on the view will be checked to ensure that new rows satisfy the view-defining condition (that is, the new rows are checked to ensure that they are visible through the view). If they are not, the update will be rejected.
Example:
CREATE TABLE a(id INT PRIMARY KEY, cola VARCHAR(10));
CREATE TABLE b(id INT PRIMARY KEY, colb VARCHAR(10), a_id INT REFERENCES a(id) NOT NULL);
CREATE TABLE c(id INT PRIMARY KEY, colc VARCHAR(10),
a_id INT REFERENCES a(id) NOT NULL,
b_id INT REFERENCES b(id));
Sample inserts:
INSERT INTO a(id, cola) VALUES (1, 'A');
INSERT INTO a(id, cola) VALUES (2, 'A2');
INSERT INTO b(id, colb, a_id) VALUES (12, 'B', 1);
INSERT INTO c(id, colc, a_id) VALUES (15, 'C', 2);
Violating the condition(connecting C with B different a_id on both tables)
UPDATE c SET b_id = 12 WHERE id = 15;;
-- no issues whatsover
Creating view:
CREATE VIEW view_c
AS
SELECT *
FROM c
WHERE NOT EXISTS(SELECT 1
FROM b
WHERE c.b_id = b.id
AND c.a_id != b.a_id) -- here is the clue, we want a_id to be the same
WITH CHECK OPTION ;
Trying update second time(error):
UPDATE view_c SET b_id = 12 WHERE id = 15;
--ERROR: new row violates check option for view "view_c"
--DETAIL: Failing row contains (15, C, 2, 12).
Trying brand new inserts with incorrect data(also errors)
INSERT INTO b(id, colb, a_id) VALUES (20, 'B2', 2);
INSERT INTO view_c(id, colc, a_id, b_id) VALUES (30, 'C2', 1, 20);
--ERROR: new row violates check option for view "view_c"
--DETAIL: Failing row contains (30, C2, 1, 20)
db<>fiddle demo
I used a COPY command to load a bunch of data in the database and then created the primary keys and foreign keys afterwards.
When I created one of the foreign keys, I got the error message: violates foreign key constraint "some_fkey" . Is there a way that I can raise the log level so that I can see on which row this violation happened so that I can go back and debug/check the data?
A workaround:
Suppose your tables look like this:
CREATE TABLE a (a_id INTEGER PRIMARY KEY, x TEXT);
CREATE TABLE b (b_id INTEGER PRIMARY KEY, a_id INTEGER, x TEXT);
And the creation of the following foreign key gives you the error:
ALTER TABLE b ADD CONSTRAINT a_a_id_fk FOREIGN KEY (a_id) REFERENCES a (a_id) ON UPDATE CASCADE ON DELETE RESTRICT;
You could join the two tables to get the problematic rows:
SELECT b_id, a_id FROM b LEFT JOIN a USING (a_id) WHERE a.a_id IS NULL;
Let's see!
Sample data:
INSERT INTO a VALUES (1, 'a'), (2, 'b'), (3, 'c');
INSERT INTO b VALUES (1, 1, 'x'), (2, 3, 'y'), (3, 1000, 'z');
And the result of the query above:
b_id | a_id
------+------
3 | 1000
(1 row)