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
Related
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
I have the following tables:
create table User
(
Id int not null primary key clustered (Id),
Name nvarchar(255) not null
)
create table dbo.UserSkill
(
UserId int not null,
SkillId int not null,
primary key clustered (UserId, SkillId)
)
Given a set of Skills Ids I need to get the users that have all these Skills Ids:
select Users.*
from Users
inner join UserSkills on Users.Id = UserSkills.UserId
where UserSkills.SkillId in (149, 305)
group by Users.Id
having count(*) = 2
I get the following error:
Column 'Users.Name' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
What am I missing?
Side questions:
Is there a faster query to accomplish the same result?
How can I pass the SkillsIds, e.g. (149, 305) as a parameter? And set the #SkillsIds count in having count(*) = 2 instead of 2?
UPDATE
The following code is working and I get the User John.
declare #Users table
(
Id int not null primary key clustered (Id),
[Name] nvarchar(255) not null
);
declare #Skills table
(
SkillId int not null primary key clustered (SkillId)
);
declare #UserSkills table
(
UserId int not null,
SkillId int not null,
primary key clustered (UserId, SkillId)
);
insert into #Users
values (1, 'John'), (2, 'Mary');
insert into #Skills
values (148), (149), (304), (305);
insert into #UserSkills
values (1, 149), (1, 305), (2, 148), (2, 149);
select u.Id, u.Name
from #Users as u
inner join #UserSkills as us on u.Id = us.UserId
where us.SkillId in (149, 305)
group by u.Id, u.Name
having count(*) = 2
If user has 40 columns, is there a way to not enumerate all the columns in the Select and Group By since Id is the only column needed to group?
First, your tables are broken, unless Name has only a single character. You need a length:
create table User (
UserId int not null primary key clustered (Id),
Name nvarchar(255) not null
);
Always use a length when specifying char(), varchar(), and related types in SQL Server.
For your query, SQL Server, is not going to process select * with group by. List each column in both the select and group by:
select u.id, u.name
from Users u join
UserSkills us
on u.Id = us.UserId
where us.SkillId in (149, 305)
group by u.Id, u.name
having count(*) = 2;
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.
I have a table called Request and data will be entered by two types of users (Company staff and site members). Initially I had a column called createdby. But, the staff and the members table primary keys are integers with identity. So i had to change it because there could be an ID that corresponds to both entities.
Then I have created two columns in the the request table ByStaffId and ByMemberID. I wonder whether it is the right way.
Given the constraints of the existing tables, your approach sounds reasonable, and means that you will be able to create foreign key constraints from the Request table to the Staff and Member tables. It would not have been possible to create any foreign key constraints with your previous approach (a single CreatedBy field).
If the opportunity to refactor the existing design is available, consider treating Staff and Members as subclasses of the same abstract type (e.g. "User"), using one of the patterns for mapping inheritance to relational tables described here.
The way thing are set up for you now what you did by creating the two columns is the solution.
But, it's not a good one. Basically you should only have one Users table with some way to differ the user types like a separate table(Roles, UserType etc). Then you would be able to have a foreign key in your Request table that will reference the user that created the initial request.
Below is an example of how to relate the Staff and Members together using a common User table that has a 1-to-1 relationship with both a StaffUser table and a MemberUser table.
Granted, this approach results in greater complexity when selecting/inserting/updating/deleting users, so you can decide whether this is worth the extra complexity.
create table [User]
(
UserID int identity(1,1) not null primary key,
Username nvarchar(50) not null
)
create table StaffUser
(
UserID int not null primary key references [User] (UserID),
FirstName nvarchar(50) not null,
LastName nvarchar(50) not null
)
create table MemberUser
(
UserID int not null primary key references [User] (UserID),
Email nvarchar(100) not null,
)
create table Request
(
ByUserID int not null references [User] (UserID),
)
declare #UserID int
insert into [User] values ('john.smith')
set #UserID = scope_identity()
insert into StaffUser values (#UserID, 'John', 'Smith')
insert into Request values (#UserID)
insert into [User] values ('billy.bob')
set #UserID = scope_identity()
insert into StaffUser values (#UserID, 'Billy', 'Bob')
insert into Request values (#UserID)
insert into [User] values ('member1')
set #UserID = scope_identity()
insert into MemberUser values (#UserID, 'member1#awesome.com')
insert into Request values (#UserID)
insert into [User] values ('member2')
set #UserID = scope_identity()
insert into MemberUser values (#UserID, 'member2#awesome.com')
insert into Request values (#UserID)
insert into [User] values ('member3')
set #UserID = scope_identity()
insert into MemberUser values (#UserID, 'member3#awesome.com')
insert into Request values (#UserID)
-- select staff
select
StaffUser.UserID,
[User].Username,
StaffUser.FirstName,
StaffUser.LastName
from StaffUser
inner join [User] on
[User].UserID = StaffUser.UserID
-- select members
select
MemberUser.UserID,
[User].Username,
MemberUser.Email
from MemberUser
inner join [User] on
[User].UserID = MemberUser.UserID
-- select all users
select
StaffUser.UserID,
[User].Username
from StaffUser
inner join [User] on
[User].UserID = StaffUser.UserID
union all
select
MemberUser.UserID,
[User].Username
from MemberUser
inner join [User] on
[User].UserID = MemberUser.UserID
select * from Request
drop table Request
drop table MemberUser
drop table StaffUser
drop table [User]
Below is a slightly more complicated structure that accomplishes the same thing as the example above, but in this case Member and Staff are more decoupled from User.
create table [User]
(
UserID int identity(1,1) not null primary key,
CreatedOn datetime not null default getdate()
)
create table StaffUser
(
UserID int not null primary key references [User] (UserID)
)
create table MemberUser
(
UserID int not null primary key references [User] (UserID)
)
create table Staff
(
StaffID int identity(1,1) not null primary key,
FirstName nvarchar(50) not null,
LastName nvarchar(50) not null,
UserID int null references StaffUser (UserID),
)
create table Member
(
MemberID int identity(1,1) not null primary key,
Username nvarchar(50),
Email nvarchar(100) not null,
UserID int null references MemberUser (UserID),
)
create table Request
(
ByUserID int not null references [User] (UserID),
)
declare #UserID int
insert into [User] default values
set #UserID = scope_identity()
insert into StaffUser values (#UserID)
insert into Staff values ('John', 'Smith', #UserID)
insert into Request values (#UserID)
insert into [User] default values
set #UserID = scope_identity()
insert into StaffUser values (#UserID)
insert into Staff values('Billy', 'Bob', #UserID)
insert into Request values (#UserID)
insert into [User] default values
set #UserID = scope_identity()
insert into MemberUser values (#UserID)
insert into Member values ('member1', 'member1#awesome.com', #UserID)
insert into Request values (#UserID)
insert into [User] default values
set #UserID = scope_identity()
insert into MemberUser values (#UserID)
insert into Member values ('member2', 'member2#awesome.com', #UserID)
insert into Request values (#UserID)
insert into [User] default values
set #UserID = scope_identity()
insert into MemberUser values (#UserID)
insert into Member values ('member3', 'member3#awesome.com', #UserID)
insert into Request values (#UserID)
-- select staff
select
Staff.StaffID,
Staff.FirstName,
Staff.LastName,
Staff.UserID
from Staff
-- select members
select
Member.MemberID,
Member.Username,
Member.Email,
Member.UserID
from Member
-- select all users
select
[User].UserID,
Staff.FirstName + ' ' + Staff.LastName as Name,
[User].CreatedOn
from [User]
inner join Staff on
Staff.UserID = [User].UserID
union all
select
[User].UserID,
Member.Username as Name,
[User].CreatedOn
from [User]
inner join Member on
Member.UserID = [User].UserID
select * from Request
drop table Request
drop table Member
drop table Staff
drop table MemberUser
drop table StaffUser
drop table [User]
Why you ever wanted to use separate tables to distinguish members from staff? I'd rather implement Role table and distinguish users by assigned roles.
.createdby column that may reference to one of these tables? Bad idea but it might work if you'd use Guid as primary key in staff and memvers
I have 3 tables in SQL Server 2008
Clubs with a PK of ID and a Name.
Products which has a PK of ID, a FK of ClubID , a Name, a ShortCode and a Keyword.
There is a UK to enforce that there are no duplicate keywords for combinations of ShortCode/Keyword.
ProductAdditionalShortCodes. This has a PK of ID, a FK of ProductID and a Keyword
The idea is to prevent any shortcode/keyword combination of products to point to different clubs and also to prevent the creation of duplicate short/code keyword combinations
I have a solution that works, but feels clunky, and could under certain circumstances fail if multiple users happened to update multiple entries at the same time. (Hypothetically)
How can I add some form of constraint to the DB to prevent the Keyword in the Main table from being the same as in the Additional table and the other way round?
Following is a sample script to create the scenario and some of the examples I want to prevent. I am not opposed to changing the DB design if the impact of the change would not disrupt too many other aspects of the solution. (I realize this is subjective)
use Tinker
if exists (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'dbo.ProductAdditionalKeywords') AND type in (N'U'))
drop table dbo.ProductAdditionalKeywords
go
if exists (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'dbo.Products') AND type in (N'U'))
drop table dbo.Products
go
if exists (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'dbo.Clubs') AND type in (N'U'))
drop table dbo.Clubs
go
create table dbo.Clubs (
ID int not null identity(1,1)
,Name varchar(50) not null
,constraint PK_Clubs primary key clustered ( ID )
)
go
alter table dbo.Clubs add constraint UK_Clubs__Name unique ( Name )
go
create table dbo.Products (
ID int not null identity(1,1)
,ClubID int not null
,Name varchar(50) not null
,ShortCode varchar(50) not null
,Keyword varchar(50) not null
,constraint PK_Products primary key clustered ( ID )
)
go
alter table dbo.Products add constraint UK_Products__ShortCode_Keyword unique ( ShortCode , Keyword )
go
alter table dbo.Products add constraint UK_Products__Name unique ( Name )
go
alter table dbo.Products add constraint FK_Products_ClubID foreign key ( ClubID ) references dbo.Clubs ( ID )
go
create table dbo.ProductAdditionalKeywords (
ID int not null identity(1,1)
,ProductID int not null
,Keyword varchar(50) not null
,constraint PK_ProductAdditionalKeywords primary key clustered ( ID )
)
go
alter table dbo.ProductAdditionalKeywords add constraint FK_ProductAdditionalKeywords_ProductID foreign key ( ProductID ) references dbo.Products ( ID )
go
alter table dbo.ProductAdditionalKeywords add constraint UK_ProductAdditionalKeywords__Keyword unique ( Keyword )
go
insert into dbo.Clubs ( Name )
select 'Club 1'
union all select 'Club 2'
insert into dbo.Products (ClubID,Name,Shortcode,Keyword)
select 1,'Product 1','001','P1'
union all select 1,'Product 2','001','P2'
union all select 1,'Product 3','001','P3'
union all select 2,'Product 4','002','P4'
union all select 2,'Product 5','002','P5'
union all select 2,'Product 6','002','P6'
insert into dbo.ProductAdditionalKeywords (ProductID,Keyword)
select 1,'P1A'
union all select 1,'P1B'
union all select 2,'P2A'
union all select 2,'P2B'
/*
What can be done to prevent the following statements from beeing allowed based on the reason in the comments?
*/
--insert into dbo.ProductAdditionalKeywords (ProductID,Keyword) values ( 1 , 'P2' ) -- Main keyword for product 2
--update dbo.Products set Keyword = 'P1A' where ID = 2 -- Additional keyword for product 1
--insert into dbo.ProductAdditionalKeywords (ProductID,Keyword) values ( 3 , 'P1' ) -- Main ShortCode/Keyword combination for product 1
/*
At the moment I look at the following view to see if the proposed(new/updated) Keyword/Shortcode combination already exists
If it already exists I pevent the insert/update
Is there any way to do it in the DB via constraints rather than in the BLL?
*/
select ShortCode,Keyword,count([ClubID]) as ClubCount from
(
select p.ClubID,p.ShortCode,p.Keyword,p.ID
from dbo.Products p
union all
select p.ClubID,p.ShortCode,PAK.Keyword,PAK.ID * -1
from dbo.ProductAdditionalKeywords as PAK
inner join dbo.Products P on PAK.ProductID = P.ID
) as FullList
group by Shortcode,Keyword
order by Shortcode,Keyword
How I would normally do this would be to place all of the keywords in a separate table (e.g. what is currently your additional table). If all keywords must be distinct within a ShortCode, then I'd include ShortCode in this table also, so that a unique constraint can be applied across both columns.
If all keywords for a product must be in the same ShortCode, then I'd keep ShortCode in Products also. I'd apply a unique constraint on (ID,ShortCode) in that table, and an additional foreign key from the keywords table, referencing both columns on both sides.
What we're left with now are two potential issues not included in your original design, but I don't know if they're a concern in practice:
1) Is the Keyword in Products more important, or special, than the additional keywords? If so, we need to add a column to keywords table to mark which one is important. To ensure only one is set, you can search for plenty of other SO questions which involve unique constraints with additional conditions. (Let me know if you can't find one and need it, I'm sure I can add a link if necessary)
2) Should a Product be allowed to have no keywords? If not, then I'd create a view that mimics your original Products table. In this circumstance, it would be easier if 1) above is true, in which case we always join to the "important" keyword. Otherwise, we need to have some way to limit it to a single row per product. We deny insert/update/delete on the table, and only allow them through the view. 3 relatively simple triggers will then maintain the underlying table structure.
on your design, I do not understand the the use of productAdditionalShortCodes having no field of ShortCode.
However, you can add Unique key constraint with ShortCode & Keyword (composite key). This will eliminate duplicate entry in product table.