Add data to many-to-many relation with one SQL command - sql

I have a basic understanding of SQL databases and I might overlooked something, but I cannot figure out the following problem: there is a many-to-many relationship (for example: users - user_roles - roles). Is it possible to add (new) role to a (new) user with one SQL command (atomic operation)? Currently I use Sqlite.
I am aware of the SELECT last_insert_rowid(); command and with this and several SQL commands I can achieve what I want. But I want to incorporate it into one command (so the server, in this case Sqlite, can optimize the query, etc.). I have no idea, how it is done in real life (one command vs. several one in one transaction), that´s the root cause of this question.
So far this is what I was able to do:
pragma foreign_keys = on;
CREATE TABLE users (
user_id integer primary key autoincrement,
user_name text not null unique
);
CREATE TABLE roles (
role_id integer primary key autoincrement,
role_name text not null unique
);
CREATE TABLE user_roles (
user_id integer not null,
role_id integer not null,
foreign key (user_id) references users(user_id),
foreign key (role_id) references roles(role_id),
primary key (user_id, role_id)
);
insert into users (user_name) values ('Joe');
insert into roles (role_name) values ('admin');
insert into user_roles (user_id, role_id) values (
(select user_id from users where user_name = 'Joe'),
(select role_id from roles where role_name = 'admin')
);
If both user and role exists (Joe and admin), then it works fine.
But I cannot figure out, how to achieve "add-if-missing-then-return-id" behavior if Joe or admin is mission from database.
Example (both user and role are missing):
insert into user_roles (user_id, role_id) values (
(select user_id from users where user_name = 'Bill'),
(select role_id from roles where role_name = 'user')
);
Result:
Execution finished with errors.
Result: NOT NULL constraint failed: user_roles.user_id

You could create view from user_roles table:
CREATE VIEW user_roles_view AS
SELECT
U.user_name, R.role_name
FROM user_roles AS UR
INNER JOIN users AS U ON u.user_id = UR.user_id
INNER JOIN roles AS R ON r.role_id = UR.role_id;
Views in SQLite are read-only unless you create an INSTEAD OF trigger on it. This way you can specify a command or sequence of commands that are executed when the view is modified using INSERT, UPDATE or DELETE statement. For INSERT it could go like this:
CREATE TRIGGER user_roles_view_insert INSTEAD OF INSERT ON user_roles_view
BEGIN
INSERT OR IGNORE INTO users (user_name) VALUES (NEW.user_name);
INSERT OR IGNORE INTO roles (role_name) VALUES (NEW.role_name);
INSERT OR IGNORE INTO user_roles (user_id, role_id) VALUES (
(SELECT user_id FROM users WHERE user_name = NEW.user_name),
(SELECT role_id FROM roles WHERE role_name = NEW.role_name)
);
END;
Note the usage of INSERT OR IGNORE to prevent inserting duplicate values into all of the three tables. Here's how you would insert values via the view:
INSERT INTO user_roles_view VALUES ('Joe', 'admin');
-- The above statement creates:
-- a row (1, 'Joe') in table users,
-- a row (1, 'admin) in table roles,
-- a row (1, 1) in table user_roles.
INSERT INTO user_roles_view VALUES ('Joe', 'admin');
-- The above statement doesn't add any additional records, because all appropriate records
-- already exist.
INSERT INTO user_roles_view VALUES ('Joe', 'system');
-- The above statement creates:
-- a row (2, 'system') in table roles,
-- a row (1, 2) in table user_roles.
INSERT INTO user_roles_view VALUES ('Alice', 'admin'), ('Bob', 'system');
-- The above statement creates:
-- a row (2, 'Alice') in table users,
-- a row (3, 'Bob') in table users,
-- a row (2, 1) in table user_roles,
-- a row (3, 2) in table user_roles
All of the above statements produce the following output from user_roles_view (SELECT * FROM user_roles_view):
user_name
role_name
Joe
admin
Joe
system
Alice
admin
Bob
system

Related

Removing Duplicates added by foriegn key in Oracle DB

I'am working on a Hostel Project in Oracle DB, I have 2 tables (students,rooms) and when I run query to get name of students ,their room IDs and facilities that are provided to them it returns duplicate student names with all the facilities and with little editing in query it returns duplicate facilities.
How can I fix those duplicates?
Here is the Code
--CREATE TABLE STUDENTS
Create table students(
regno integer primary key,
name varchar2(30),
phonenum number,
address varchar2(30),
roomalloc number
);
Alter Table students Add CONSTRAINT fk_roomalloc
FOREIGN KEY (roomalloc)
REFERENCES rooms(roomno);
Alter table students drop constraint fk_roomalloc
--ALTER TABLE STUDENTS ADD JOIN DATE
alter table students add (joindate Date);
--Alter Table for Gender
Alter Table students add(gender varchar2(2));
Alter Table students MODIFY gender NOT NULL;
--Constraint on Gender
Alter Table students Add CONSTRAINT gen
Check (gender IN('M','F','m','f'));
Alter Table students MODIFY mess default 'M';
--Constraint Gender Check for Room Allocation
Alter Table students Add CONSTRAINTS gen_check
Check (Room IN(Gender='M' , 'Gender='F'));
--Constraints:ADD MESS AS TRUE/FALSE
ALTER TABLE students ADD (mess varchar2(4));
Alter Table students Add CONSTRAINT mess_present
Check (mess IN('Yes','No','yes','no'));
Alter Table students MODIFY mess default 'No';
--Drop Constraint
ALTER TABLE students
DROP CONSTRAINT mess_present
--ALTER TABLE NAME AS NOT NULL
alter table students MODIFY name NOT NULL;
--ALTER TABLE PHONE NO
alter table students MODIFY phonenum UNIQUE;
--ALTER TABLE Room Allocated
alter table students MODIFY roomalloc Not Null;
Describe students;
--DATA ENTRY and Modification
INSERT INTO students(regno, name, phonenum, address, roomalloc,joindate,mess)
VALUES (1, 'Haseeb', 012345678,'Rawalpindi',1,'1-sep-14','No');
INSERT INTO students(regno, name, phonenum, address, roomalloc,joindate)
VALUES (2, 'Faisal', 03451111111,'Rawalpindi',1,'12-sep-14');
INSERT INTO students(regno, name, phonenum, address, roomalloc,joindate)
VALUES (3, 'Shahbaz', 03313214567,'Khewra',1,'15-feb-15');
INSERT INTO students(regno, name, phonenum, address, roomalloc,joindate)
VALUES (4, 'Muhaddas', 01235131237,'Kashmir',1,'15-feb-15');
INSERT INTO students(regno, name, phonenum, address, roomalloc,joindate,mess)
VALUES (5, 'Haseem', 01254530987,'Islamabad',2,'15-sep-15','Yes');
INSERT INTO students(regno, name, phonenum, address, roomalloc,joindate,mess)
VALUES (6, 'Asim', 03341234567,'Muzzafargarh',3,'15-sep-14','Yes');
INSERT INTO students(regno, name, phonenum, address, roomalloc,joindate,mess)
VALUES (7, 'Izza', 01231234564,'Sialkot',25,'15-sep-15','Yes');
INSERT INTO students(regno, name, phonenum, address, roomalloc,joindate,mess)
VALUES (8, 'Sara', 01231234561,'Narrowal',25,'15-sep-15','Yes');
INSERT INTO students(regno, name, phonenum, address, roomalloc,joindate,mess)
VALUES (9, 'Maria', 01231234567,'Wah',4,'25-sep-15','Yes');
INSERT INTO students(regno, name, phonenum, address, roomalloc,joindate,Gender)
VALUES (10, 'Faha', 0123123452,'Okara',4,'26-sep-15','F');
Update students SET Gender='M' WHERE regno>=1 And regno<=6;
Update students SET roomalloc=4,Gender='F' WHERE regno>=7 And regno<=9;
Describe students;
--Data Fetching Queries
Select *
From students,rooms
Where rooms.facility='Fan/Net/TV'
ORDER BY roomalloc , regno;
Drop table students;
--CREATE TABLE Rooms
Create table rooms(
roomno integer primary key,
facility varchar2(30)
);
--Altering Rooms Types
Alter Table rooms Add CONSTRAINT rtype
Check (facility IN('AC/Net/TV','Fan/Net/TV','Fan/Net'));
Alter Table rooms MODIFY facility default 'Fan/Net';
Alter table rooms drop constraint str
INSERT INTO rooms( roomno, facility)
VALUES (1,'AC/Net/TV');
INSERT INTO rooms( roomno, facility)
VALUES (2,'Fan/Net/TV');
INSERT INTO rooms( roomno, facility)
VALUES (3,'Fan/Net/TV');
INSERT INTO rooms( roomno, facility)
VALUES (4,'Fan/Net');
Alter table rooms Modify facility UNIQUE
Select *
From rooms;
Select students.name,rooms.facility
From students,rooms
Where roomno=4;
Drop table rooms
You need to join the tables properly to get the desired result:
Select students.name,rooms.facility
From students join rooms on students.roomalloc = rooms.roomno
Where rooms.roomno=4;

Using SELECT in INSERT in Derby SQL

I have the following SQL code to create and populate a few tables in Derby:
CREATE TABLE GROUPS (
GRP_ID INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1) PRIMARY KEY,
GRP_NAME VARCHAR(256) NOT NULL,
DISPLAY_NAME VARCHAR(256));
CREATE TABLE USERS (
USR_ID INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1) PRIMARY KEY,
USR_NAME VARCHAR(256) NOT NULL,
PASSWORD VARCHAR(512) NOT NULL,
DISPLAY_NAME VARCHAR(256));
CREATE TABLE USR_GRP_MEMBERSHIP (
USR_ID INTEGER NOT NULL REFERENCES USERS ON DELETE CASCADE,
GRP_ID INTEGER NOT NULL REFERENCES GROUPS ON DELETE CASCADE,
CONSTRAINT USR_ID_GRP_ID UNIQUE (USR_ID, GRP_ID));
INSERT INTO GROUPS(GRP_NAME, DISPLAY_NAME) VALUES('Users', 'Users');
INSERT INTO GROUPS(GRP_NAME, DISPLAY_NAME) VALUES('AdminUsers', 'Administrators');
INSERT INTO USERS(USR_NAME, PASSWORD, DISPLAY_NAME) VALUES('user', 'userpwd', 'User');
INSERT INTO USERS(USR_NAME, PASSWORD, DISPLAY_NAME) VALUES('admin', 'adminpawd', 'Administrator');
#The following code populates the USR_GRP_MEMBERSHIP table using the IDs of users
#& groups inserted in the previous set of insert statements
INSERT INTO USR_GRP_MEMBERSHIP VALUES((SELECT USR_ID FROM USERS WHERE USR_NAME = 'user'), (SELECT GRP_ID FROM GROUPS WHERE GRP_NAME = 'Users'));
INSERT INTO USR_GRP_MEMBERSHIP VALUES((SELECT USR_ID FROM USERS WHERE USR_NAME = 'admin'), (SELECT GRP_ID FROM GROUPS WHERE GRP_NAME = 'Users'));
INSERT INTO USR_GRP_MEMBERSHIP VALUES((SELECT USR_ID FROM USERS WHERE USR_NAME = 'admin'), (SELECT GRP_ID FROM GROUPS WHERE GRP_NAME = 'AdminUsers'));
My Question is: The last 3 INSERT statements have the SELECT statements repeating in them. How do I store the output of those SELECT statements in a variable and use them in these 3 INSERT statements?
One way to accomplish this is to use placeholders in prepared statements?
Here's a good starting point: https://docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html
Your statement would then look something like:
INSERT INTO USR_GRP_MEMBERSHIP
VALUES ((SELECT USR_ID FROM USERS WHERE USR_NAME = ?),
(SELECT GRP_ID FROM GROUPS WHERE GRP_NAME = ?));
and you would replace the placeholders with 'user'/'Users', 'admin'/'Users', 'admin'/'AdminUsers' at runtime by using SetString (twice) and then ExecuteUpdate, for each pair.

Complex SQL Count Query

Hello I've been stuck with one SQL query for my assignment and was hoping for some help.
I need to get the Project ID for the best executed project -the project where (VERY_GOOD record count + GOOD record count) - (VERY_BAD record count + BAD record count) is greatest
My schema and test records in database (HSQLDB)
CREATE TABLE
PROJECT
(
ID IDENTITY NOT NULL PRIMARY KEY,
PROJECT_NAME VARCHAR(255) NOT NULL
);
CREATE TABLE
RECORD
(
ID IDENTITY NOT NULL PRIMARY KEY,
RESULT VARCHAR(255) NOT NULL,
);
CREATE TABLE
RECORD_PROJECT
(
PROJECT_ID INTEGER NOT NULL,
RECORD_ID INTEGER NOT NULL,
PRIMARY KEY(PROJECT_ID, RECORD_ID),
FOREIGN KEY (PROJECT_ID) REFERENCES PROJECT(ID) ON DELETE CASCADE,
FOREIGN KEY (RECORD_ID) REFERENCES RECORD(ID)
);
And test data:
INSERT INTO PROJECT (PROJECT_NAME) VALUES ('Bake a cake');
INSERT INTO PROJECT (PROJECT_NAME) VALUES ('Clean the house');
INSERT INTO RECORD (RESULT) VALUES ('GOOD');
INSERT INTO RECORD (RESULT) VALUES ('VERY_GOOD');
INSERT INTO RECORD (RESULT) VALUES ('VERY_GOOD');
INSERT INTO RECORD (RESULT) VALUES ('BAD');
INSERT INTO RECORD (RESULT) VALUES ('VERY_BAD');
INSERT INTO RECORD_PROJECT (PROJECT_ID, RECORD_ID) VALUES (0,0);
INSERT INTO RECORD_PROJECT (PROJECT_ID, RECORD_ID) VALUES (1,1);
INSERT INTO RECORD_PROJECT (PROJECT_ID, RECORD_ID) VALUES (1,2);
INSERT INTO RECORD_PROJECT (PROJECT_ID, RECORD_ID) VALUES (0,3);
INSERT INTO RECORD_PROJECT (PROJECT_ID, RECORD_ID) VALUES (1,4);
(I removed unrelated fields from tables)
So with this data I have 3 good records and 2 bad, I would need to get the project which has the highest 'rating', which according to this right now would be Clean the house with 3 good ratings over 2 negative for other project.
Maybe someone would figure this out, thanks!
That should be the (not testet) SQL in MySQL-Dialect:
SELECT rp.PROJECT_ID, p.PROJECT_NAME
SUM(CASE WHEN rp.RECORD_ID < 3 THEN 1 ELSE 0 END) AS rating
FROM RECORD_PROJEKT AS rp
JOIN PROJECT AS p ON p.ID = rp.PROJECT_ID
GROUP BY rp.PROJECT_ID
ORDER BY rating DESC

Insert data in 3 tables at a time using Postgres

I want to insert data into 3 tables with a single query.
My tables looks like below:
CREATE TABLE sample (
id bigserial PRIMARY KEY,
lastname varchar(20),
firstname varchar(20)
);
CREATE TABLE sample1(
user_id bigserial PRIMARY KEY,
sample_id bigint REFERENCES sample,
adddetails varchar(20)
);
CREATE TABLE sample2(
id bigserial PRIMARY KEY,
user_id bigint REFERENCES sample1,
value varchar(10)
);
I will get a key in return for every insertion and I need to insert that key in the next table.
My query is:
insert into sample(firstname,lastname) values('fai55','shaggk') RETURNING id;
insert into sample1(sample_id, adddetails) values($id,'ss') RETURNING user_id;
insert into sample2(user_id, value) values($id,'ss') RETURNING id;
But if I run single queries they just return values to me and I cannot reuse them in the next query immediately.
How to achieve this?
Use data-modifying CTEs:
WITH ins1 AS (
INSERT INTO sample(firstname, lastname)
VALUES ('fai55', 'shaggk')
-- ON CONFLICT DO NOTHING -- optional addition in Postgres 9.5+
RETURNING id AS sample_id
)
, ins2 AS (
INSERT INTO sample1 (sample_id, adddetails)
SELECT sample_id, 'ss' FROM ins1
RETURNING user_id
)
INSERT INTO sample2 (user_id, value)
SELECT user_id, 'ss2' FROM ins2;
Each INSERT depends on the one before. SELECT instead of VALUES makes sure nothing is inserted in subsidiary tables if no row is returned from a previous INSERT. (Since Postgres 9.5+ you might add an ON CONFLICT.)
It's also a bit shorter and faster this way.
Typically, it's more convenient to provide complete data rows in one place:
WITH data(firstname, lastname, adddetails, value) AS (
VALUES -- provide data here
('fai55', 'shaggk', 'ss', 'ss2') -- see below
, ('fai56', 'XXaggk', 'xx', 'xx2') -- works for multiple input rows
-- more?
)
, ins1 AS (
INSERT INTO sample (firstname, lastname)
SELECT firstname, lastname -- DISTINCT? see below
FROM data
-- ON CONFLICT DO NOTHING -- UNIQUE constraint? see below
RETURNING firstname, lastname, id AS sample_id
)
, ins2 AS (
INSERT INTO sample1 (sample_id, adddetails)
SELECT ins1.sample_id, d.adddetails
FROM data d
JOIN ins1 USING (firstname, lastname)
RETURNING sample_id, user_id
)
INSERT INTO sample2 (user_id, value)
SELECT ins2.user_id, d.value
FROM data d
JOIN ins1 USING (firstname, lastname)
JOIN ins2 USING (sample_id);
db<>fiddle here
You may need explicit type casts in a stand-alone VALUES expression - as opposed to a VALUES expression attached to an INSERT where data types are derived from the target table. See:
Casting NULL type when updating multiple rows
If multiple rows can come with identical (firstname, lastname), you may need to fold duplicates for the first INSERT:
...
INSERT INTO sample (firstname, lastname)
SELECT DISTINCT firstname, lastname FROM data
...
You could use a (temporary) table as data source instead of the CTE data.
It would probably make sense to combine this with a UNIQUE constraint on (firstname, lastname) in the table and an ON CONFLICT clause in the query.
Related:
How to use RETURNING with ON CONFLICT in PostgreSQL?
Is SELECT or INSERT in a function prone to race conditions?
Something like this
with first_insert as (
insert into sample(firstname,lastname)
values('fai55','shaggk')
RETURNING id
),
second_insert as (
insert into sample1( id ,adddetails)
values
( (select id from first_insert), 'ss')
RETURNING user_id
)
insert into sample2 ( id ,adddetails)
values
( (select user_id from first_insert), 'ss');
As the generated id from the insert into sample2 is not needed, I removed the returning clause from the last insert.
Typically, you'd use a transaction to avoid writing complicated queries.
http://www.postgresql.org/docs/current/static/sql-begin.html
http://dev.mysql.com/doc/refman/5.7/en/commit.html
You could also use a CTE, assuming your Postgres tag is correct. For instance:
with sample_ids as (
insert into sample(firstname, lastname)
values('fai55','shaggk')
RETURNING id
), sample1_ids as (
insert into sample1(id, adddetails)
select id,'ss'
from sample_ids
RETURNING id, user_id
)
insert into sample2(id, user_id, value)
select id, user_id, 'val'
from sample1_ids
RETURNING id, user_id;
You could create an after insert trigger on the Sample table to insert into the other two tables.
The only issue i see with doing this is that you wont have a way of inserting adddetails it will always be empty or in this case ss. There is no way to insert a column into sample thats not actualy in the sample table so you cant send it along with the innital insert.
Another option would be to create a stored procedure to run your inserts.
You have the question taged mysql and postgressql which database are we talking about here?

insert foreach x where not exists

I have a fairly simple database consisting of three (relevant) tables: users, permissions, and user permissions. The basic premise is simple: when a user gets created, all the records in the permissions table are automatically added to the user_permissions table with a default value. This is all working fine.
However, as I'm currently in development, I continue to add new permissions, which of course existing users won't have since those new permissions didn't exist in the permissions table when they were created. So, I had the brilliant idea to create a little stored procedure to automatically update the user_permissions table with all the permissions not currently existing in the user_permissions table.
So, in short, what I want to do is something like (pseudocode)
For each user without x permission in user_permissions, insert into user_permissions user_id and permission_id
I wasn't quite sure how to do this from an SQL POV. I played with joins and "not exists" but haven't really gotten anywhere.
You can play with my schema here: http://sqlfiddle.com/#!3/b0761/3
Thanks in advance for the help!
EDIT: Schema:
CREATE TABLE users (
user_id int IDENTITY(1, 1) NOT NULL,
user_name varchar(255),
PRIMARY KEY (user_id));
CREATE TABLE permissions (
permission_id int IDENTITY(1, 1) NOT NULL,
permission_name varchar(255) NOT NULL,
PRIMARY KEY (permission_id));
CREATE TABLE user_permissions (
user_id int NOT NULL,
permission_id int NOT NULL,
value tinyint DEFAULT 0 NOT NULL,
PRIMARY KEY (user_id,
permission_id));
ALTER TABLE user_permissions ADD CONSTRAINT FK_user_pe338140
FOREIGN KEY (permission_id)
REFERENCES permissions (permission_id);
ALTER TABLE user_permissions ADD CONSTRAINT FK_user_pe405324
FOREIGN KEY (user_id) REFERENCES users (user_id);
INSERT INTO users(user_name) values('test_username');
INSERT INTO users(user_name) values('test_username2');
INSERT INTO permissions(permission_name) VALUES('permission_1')
INSERT INTO permissions(permission_name) VALUES('permission_2')
INSERT INTO user_permissions(user_id, permission_id, value)
VALUES(1, 1, 1)
INSERT INTO user_permissions(user_id, permission_id, value)
VALUES(2, 1, 1)
EDIT: Query so far
SELECT a.user_id, b.permission_id, 1 as 'value'
FROM USER_PERMISSIONS a right outer join
PERMISSIONS b on a.permission_id = b.permission_id
insert into user_permissions (user_id, permission_id)
select
u.user_id,
p.permission_id
from
users u
cross join permissions p
where
not exists (select 0 from user_permissions with (updlock, holdlock)
where user_id = u.user_id and permission_id = p.permission_id)
Reference: Only inserting a row if it's not already there
INSERT dbo.user_permissions([user_id], permission_id, [value])
SELECT u.[user_id], p.permission_id, 1
FROM dbo.user_permissions AS u
CROSS JOIN dbo.[permissions] AS p
WHERE NOT EXISTS (SELECT 1 FROM dbo.user_permissions
WHERE [user_id] = u.[user_id]
AND permission_id = p.permission_id
)
GROUP BY [user_id], p.permission_id;
As an aside, you should avoid names that tend to require delimiters, e.g. user_id and permissions are keywords/reserved words.
Mansfield,
If I understand you correctly, you want to seed the user_permissions table with a value when you add a new permission.
I'll also assume that you want it to default to 0. After inserting the new permission, running this code will seed the user_permissions table for all users with a default value of 0 for any permissions currently not in use.
--insert into permissions(permission_name) values('newperm');
--select * from permissions
insert into user_permissions(user_id, permission_id, value)
select
u.user_id, p.permission_id, 0
from
users u
cross join permissions p
where
p.permission_id not in(select permission_id from user_permissions)
;
--select * from user_permissions;
The below query will give you the missing userpermission rows to be inserted:
select a.USER_ID,b.permission_id from users a,permissions b,user_permissions c
where c.user_id <>a.user_id and c.permission_id <> b.permission_id